Compare commits

..

2 Commits

Author SHA1 Message Date
GreemDev
e4abc3a960 cleanup 2026-01-04 19:34:32 -06:00
GreemDev
8ccbf33327 Replace CommandLineState with a more user-friendly CLI experience. 2026-01-04 05:18:32 -06:00
76 changed files with 2175 additions and 2772 deletions

View File

@@ -44,7 +44,7 @@
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" /> <PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" /> <PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" /> <PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
<PackageVersion Include="Gommon" Version="2.8.0.1" /> <PackageVersion Include="Gommon" Version="2.8.0.4" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" /> <PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.11.1" /> <PackageVersion Include="Sep" Version="0.11.1" />
<PackageVersion Include="shaderc.net" Version="0.1.0" /> <PackageVersion Include="shaderc.net" Version="0.1.0" />
@@ -59,4 +59,4 @@
<PackageVersion Include="System.Management" Version="9.0.2" /> <PackageVersion Include="System.Management" Version="9.0.2" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" /> <PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -47,8 +47,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Vic", "src
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Video", "src\Ryujinx.Graphics.Video\Ryujinx.Graphics.Video.csproj", "{FD4A2C14-8E3D-4957-ABBE-3C38897B3E2D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Video", "src\Ryujinx.Graphics.Video\Ryujinx.Graphics.Video.csproj", "{FD4A2C14-8E3D-4957-ABBE-3C38897B3E2D}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.Apple", "src\Ryujinx.Audio.Backends.Apple\Ryujinx.Audio.Backends.Apple.csproj", "{AC26EFF0-8593-4184-9A09-98E37EFFB32E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SDL3", "src\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj", "{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SDL3", "src\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj", "{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.OpenAL", "src\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj", "{0BE11899-DF2D-4BDE-B9EE-2489E8D35E7D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.OpenAL", "src\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj", "{0BE11899-DF2D-4BDE-B9EE-2489E8D35E7D}"
@@ -571,8 +569,6 @@ Global
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU {D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -7,12 +7,12 @@
"de_DE": "", "de_DE": "",
"el_GR": "", "el_GR": "",
"en_US": "Start RenderDoc Frame Capture", "en_US": "Start RenderDoc Frame Capture",
"es_ES": "Iniciar una captura de fotograma de RenderDoc", "es_ES": "",
"fr_FR": "Démarrer une capture de trame RenderDoc", "fr_FR": "",
"he_IL": "", "he_IL": "",
"it_IT": "", "it_IT": "",
"ja_JP": "", "ja_JP": "",
"ko_KR": "RenderDoc 프레임 캡처 시작", "ko_KR": "",
"no_NO": "", "no_NO": "",
"pl_PL": "", "pl_PL": "",
"pt_BR": "", "pt_BR": "",
@@ -21,7 +21,7 @@
"th_TH": "", "th_TH": "",
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "启动 RenderDoc 帧捕获", "zh_CN": "",
"zh_TW": "" "zh_TW": ""
} }
}, },
@@ -32,12 +32,12 @@
"de_DE": "", "de_DE": "",
"el_GR": "", "el_GR": "",
"en_US": "End RenderDoc Frame Capture", "en_US": "End RenderDoc Frame Capture",
"es_ES": "Detener la captura de fotograma de RenderDoc", "es_ES": "",
"fr_FR": "Arrêter la capture de trame RenderDoc", "fr_FR": "",
"he_IL": "", "he_IL": "",
"it_IT": "", "it_IT": "",
"ja_JP": "", "ja_JP": "",
"ko_KR": "RenderDoc 프레임 캡처 종료", "ko_KR": "",
"no_NO": "", "no_NO": "",
"pl_PL": "", "pl_PL": "",
"pt_BR": "", "pt_BR": "",
@@ -46,7 +46,7 @@
"th_TH": "", "th_TH": "",
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "结束 RenderDoc 帧捕获", "zh_CN": "",
"zh_TW": "" "zh_TW": ""
} }
}, },
@@ -57,12 +57,12 @@
"de_DE": "", "de_DE": "",
"el_GR": "", "el_GR": "",
"en_US": "Discard RenderDoc Frame Capture", "en_US": "Discard RenderDoc Frame Capture",
"es_ES": "Descartar la captura de fotograma de RenderDoc", "es_ES": "",
"fr_FR": "Supprimer la capture de trame RenderDoc", "fr_FR": "",
"he_IL": "", "he_IL": "",
"it_IT": "", "it_IT": "",
"ja_JP": "", "ja_JP": "",
"ko_KR": "RenderDoc 프레임 캡처 폐기", "ko_KR": "",
"no_NO": "", "no_NO": "",
"pl_PL": "", "pl_PL": "",
"pt_BR": "", "pt_BR": "",
@@ -71,7 +71,7 @@
"th_TH": "", "th_TH": "",
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "丢弃 RenderDoc 帧捕获", "zh_CN": "",
"zh_TW": "" "zh_TW": ""
} }
}, },
@@ -82,12 +82,12 @@
"de_DE": "", "de_DE": "",
"el_GR": "", "el_GR": "",
"en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.", "en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.",
"es_ES": "Finaliza la captura de fotograma de RenderDoc actualmente activa y descarta inmediatamente su resultado.", "es_ES": "",
"fr_FR": "Met fin à la capture de trame RenderDoc en cours, en supprimant immédiatement son résultat.", "fr_FR": "",
"he_IL": "", "he_IL": "",
"it_IT": "", "it_IT": "",
"ja_JP": "", "ja_JP": "",
"ko_KR": "현재 활성화된 RenderDoc 프레임 캡처를 종료하고 결과를 즉시 폐기합니다.", "ko_KR": "",
"no_NO": "", "no_NO": "",
"pl_PL": "", "pl_PL": "",
"pt_BR": "", "pt_BR": "",
@@ -96,7 +96,7 @@
"th_TH": "", "th_TH": "",
"tr_TR": "", "tr_TR": "",
"uk_UA": "", "uk_UA": "",
"zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。", "zh_CN": "",
"zh_TW": "" "zh_TW": ""
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,854 +0,0 @@
{
"Locales": [
{
"ID": "MenuBarOptions_OpenUserProfiles",
"Translations": {
"ar_SA": "_ملفات المستخدمين",
"de_DE": "_Benutzerprofile",
"el_GR": "_Προφίλ Χρηστών",
"en_US": "_User Profiles",
"es_ES": "_Perfiles de Usuario",
"fr_FR": "_Profils d'Utilisateurs",
"he_IL": "_פרופילי משתמש",
"it_IT": "_Profili utent",
"ja_JP": "ユーザプロファイル(_M)",
"ko_KR": "사용자 프로필(_M)",
"no_NO": "_Brukerprofiler",
"pl_PL": "_Profile użytkowników",
"pt_BR": "_Perfis de usuário",
"ru_RU": "_Учётные записи",
"sv_SE": "_Användarprofiler",
"th_TH": "_โปรไฟล์ผู้ใช้งาน",
"tr_TR": "_Kullanıcı Profilleri",
"uk_UA": "_Профілі користувачів",
"zh_CN": "用户配置文件(_M)",
"zh_TW": "使用者設定檔(_M)"
}
},
{
"ID": "WindowTitle",
"Translations": {
"ar_SA": "ملفات المستخدمين",
"de_DE": "Benutzerprofile",
"el_GR": "Προφίλ Χρηστών",
"en_US": "User Profiles",
"es_ES": "Perfiles de Usuario",
"fr_FR": "Profils d'Utilisateurs",
"he_IL": "פרופילי משתמש",
"it_IT": "Profili utent",
"ja_JP": "ユーザプロファイル",
"ko_KR": "사용자 프로필",
"no_NO": "Brukerprofiler",
"pl_PL": "Profile użytkowników",
"pt_BR": "Perfis de usuário",
"ru_RU": "Учётные записи",
"sv_SE": "Användarprofiler",
"th_TH": "โปรไฟล์ผู้ใช้งาน",
"tr_TR": "Kullanıcı Profilleri",
"uk_UA": "Профілі користувачів",
"zh_CN": "用户配置文件",
"zh_TW": "使用者設定檔"
}
},
{
"ID": "ManageSaves",
"Translations": {
"ar_SA": "عمليات الحفظ",
"de_DE": "Speicherstände",
"el_GR": "Αποθηκεύσεις",
"en_US": "Saves",
"es_ES": "Partidas",
"fr_FR": "Sauvegardes",
"he_IL": "שמירות",
"it_IT": "Salvataggi",
"ja_JP": "セーブデータ",
"ko_KR": "저장",
"no_NO": "Lagringer",
"pl_PL": "Zapisy",
"pt_BR": "Salvamentos",
"ru_RU": "Сохранения",
"sv_SE": "Sparningar",
"th_TH": "บันทึก",
"tr_TR": "Kayıtlar",
"uk_UA": "Збереження",
"zh_CN": "存档",
"zh_TW": "存檔"
}
},
{
"ID": "DeleteSaveNote",
"Translations": {
"ar_SA": "هل حذف بيانات حفظ المستخدم لهذه اللعبة؟",
"de_DE": "Löschen Sie die gespeicherten Spielstände dieses Spiels?",
"el_GR": "Διαγραφή των δεδομένων αποθήκευσης αυτού του παιχνιδιού;",
"en_US": "Delete this game's save data?",
"es_ES": "¿Eliminar los datos de guardado de este juego?",
"fr_FR": "Supprimer les données de sauvegarde de ce jeu ?",
"he_IL": "האם למחוק את נתוני השמירה של המשחק הזה?",
"it_IT": "Eliminare i dati di salvataggio di questo gioco?",
"ja_JP": "このゲームのセーブデータを削除しますか?",
"ko_KR": "이 게임의 저장 데이터를 삭제하시겠습니까?",
"no_NO": "Slette lagrede data for dette spillet?",
"pl_PL": "Usunąć dane zapisu dla tej gry?",
"pt_BR": "Excluir os dados salvos deste jogo?",
"ru_RU": "Удалить данные сохранений для этой игры?",
"sv_SE": "Ta bort sparad data för detta spel?",
"th_TH": "ลบข้อมูลบันทึกของเกมนี้หรือไม่?",
"tr_TR": "Bu oyun için kaydedilen veriyi silmek?",
"uk_UA": "Видалити збереження даних для цієї гри?",
"zh_CN": "删除此游戏的存档数据?",
"zh_TW": "刪除此遊戲的存檔資料?"
}
},
{
"ID": "SaveManagerTitle",
"Translations": {
"ar_SA": "حفظات {0}",
"de_DE": "{0}s Speicherstände",
"el_GR": "Αποθηκεύσεις του {0}",
"en_US": "{0}'s Saves",
"es_ES": "Guardados de {0}",
"fr_FR": "Sauvegardes de {0}",
"he_IL": "שמירות של {0}",
"it_IT": "Salvataggi di {0}",
"ja_JP": "{0} のセーブデータ",
"ko_KR": "{0} 의 저장",
"no_NO": "Lagringer til {0}",
"pl_PL": "Zapisy {0}",
"pt_BR": "Salvamentos de {0}",
"ru_RU": "Сохранения {0}",
"sv_SE": "{0}s Sparningar",
"th_TH": "ข้อมูลที่บันทึกไว้ของ {0}",
"tr_TR": "{0}nin Kayıtları",
"uk_UA": "Збереження {0}",
"zh_CN": "{0} 的存档",
"zh_TW": "{0} 的存檔"
}
},
{
"ID": "RecoverLostProfiles",
"Translations": {
"ar_SA": "الملفات الشخصية المفقودة",
"de_DE": "Verlorene Profile",
"el_GR": "Χαμένα προφίλ",
"en_US": "Lost Profiles",
"es_ES": "Perfiles Perdidos",
"fr_FR": "Profils Perdus",
"he_IL": "פרופילים אבודים",
"it_IT": "Profili persi",
"ja_JP": "失われたプロフィール",
"ko_KR": "분실된 프로필",
"no_NO": "Tapte profiler",
"pl_PL": "Utracone profile",
"pt_BR": "Perfis perdidos",
"ru_RU": "Потерянные учёные записи",
"sv_SE": "Förlorade profiler",
"th_TH": "โปรไฟล์ที่สูญหาย",
"tr_TR": "Kayıp profiller",
"uk_UA": "Втрачені профілі",
"zh_CN": "丢失的个人资料",
"zh_TW": "遺失的個人資料"
}
},
{
"ID": "RecoverLostProfiles_ToolTip",
"Translations": {
"ar_SA": "يستعيد الملفات الشخصية التي لم تُحذف يدويًا والتي تحتوي على حفظات.",
"de_DE": "Stellt nicht manuell gelöschte Profile mit Speicherständen wieder.",
"el_GR": "Ανακτά προφίλ που δεν διαγράφηκαν χειροκίνητα και έχουν αποθηκεύσεις.",
"en_US": "Recovers non-manually-deleted profiles that have saves.",
"es_ES": "Recupera perfiles no eliminados manualmente que tienen guardados.",
"fr_FR": "Récupère les profils non supprimés manuellement ayant des sauvegardes.",
"he_IL": "שחזור פרופילים שלא נמחקו ידנית ויש להם שמירות.",
"it_IT": "Recupera profili non eliminati manualmente che hanno salvataggi.",
"ja_JP": "手動で削除されていない、保存されたプロフィールを回復します。",
"ko_KR": "수동으로 삭제되지 않은 저장된 프로필을 복구합니다.",
"no_NO": "Gjenoppretter profiler som ikke er manuelt slettet og som har lagringer.",
"pl_PL": "Odzyskuje profile, które nie zostały usunięte ręcznie, a które mają zapisy.",
"pt_BR": "Recupera perfis não deletados manualmente que possuem saves.",
"ru_RU": "Восстанавливает учётные записи, не удалённые вручную и имеющие сохранения.",
"sv_SE": "Återställer profiler som inte har raderats manuellt och har sparade data.",
"th_TH": "กู้คืนโปรไฟล์ที่ไม่ได้ลบด้วยตนเองและมีการบันทึก",
"tr_TR": "Manuel olarak silinmemiş ve kayıtlara sahip profilleri kurtarır.",
"uk_UA": "Відновлює учётні записи, які не були видалені вручну і мають збереження.",
"zh_CN": "恢复未手动删除且有存档的个人资料。",
"zh_TW": "恢復未手動刪除且有存檔的個人資料。"
}
},
{
"ID": "RecoverProfile",
"Translations": {
"ar_SA": "استعادة",
"de_DE": "Wiederherstellen",
"el_GR": "Ανάκτηση",
"en_US": "Recover",
"es_ES": "Recuperar",
"fr_FR": "Récupérer",
"he_IL": "שחזר",
"it_IT": "Recupera",
"ja_JP": "復旧",
"ko_KR": "복구",
"no_NO": "Gjenopprett",
"pl_PL": "Odzyskaj",
"pt_BR": "Recuperar",
"ru_RU": "Восстановить",
"sv_SE": "Återskapa",
"th_TH": "กู้คืน",
"tr_TR": "Kurtar",
"uk_UA": "Відновити",
"zh_CN": "恢复",
"zh_TW": "復原"
}
},
{
"ID": "RecoverProfile_EmptyList",
"Translations": {
"ar_SA": "لا توجد ملفات شخصية لاستردادها",
"de_DE": "Keine Profile zum Wiederherstellen",
"el_GR": "Δεν υπάρχουν προφίλ για ανάκτηση",
"en_US": "No Profiles To Recover",
"es_ES": "No hay perfiles a recuperar",
"fr_FR": "Aucun profil à restaurer",
"he_IL": "אין פרופילים לשחזור",
"it_IT": "Nessun profilo da recuperare",
"ja_JP": "復元するプロファイルはありません",
"ko_KR": "복구할 프로필 없음",
"no_NO": "Ingen profiler å gjenopprette",
"pl_PL": "Brak profili do odzyskania",
"pt_BR": "Nenhum perfil para recuperar",
"ru_RU": "Нет учётных записей для восстановления",
"sv_SE": "Inga profiler att återskapa",
"th_TH": "ไม่มีโปรไฟล์ที่สามารถกู้คืนได้",
"tr_TR": "Kurtarılacak profil bulunamadı",
"uk_UA": "Немає профілів для відновлення",
"zh_CN": "没有可以恢复的用户数据",
"zh_TW": "無設定檔可復原"
}
},
{
"ID": "ManageSaves_SortByName",
"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": "ManageSaves_SortBySize",
"Translations": {
"ar_SA": "الحجم",
"de_DE": "Größe",
"el_GR": "Μέγεθος",
"en_US": "Size",
"es_ES": "Tamaño",
"fr_FR": "Taille",
"he_IL": "גודל",
"it_IT": "Dimensione",
"ja_JP": "サイズ",
"ko_KR": "크기",
"no_NO": "Størrelse",
"pl_PL": "Rozmiar",
"pt_BR": "Tamanho",
"ru_RU": "Размер",
"sv_SE": "Storlek",
"th_TH": "ขนาด",
"tr_TR": "Boyut",
"uk_UA": "Розмір",
"zh_CN": "大小",
"zh_TW": "大小"
}
},
{
"ID": "ManageSaves_SortOrderAscending",
"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": "ManageSaves_SortOrderDescending",
"Translations": {
"ar_SA": "تنازلي",
"de_DE": "Absteigend",
"el_GR": "Φθίνουσα",
"en_US": "Descending",
"es_ES": "Descendente",
"fr_FR": "Décroissant",
"he_IL": "סדר יורד",
"it_IT": "Decrescente",
"ja_JP": "降順",
"ko_KR": "내림차순",
"no_NO": "Synkende",
"pl_PL": "Malejąco",
"pt_BR": "Descendente",
"ru_RU": "По Убыванию",
"sv_SE": "Fallande",
"th_TH": "จากมากไปน้อย",
"tr_TR": "Azalan",
"uk_UA": "За спаданням",
"zh_CN": "降序",
"zh_TW": "從大到小"
}
},
{
"ID": "ManageSaves_Search",
"Translations": {
"ar_SA": "بحث",
"de_DE": "Suche",
"el_GR": "Αναζήτηση",
"en_US": "Search",
"es_ES": "Buscar",
"fr_FR": "Rechercher",
"he_IL": "חפש",
"it_IT": "Cerca",
"ja_JP": "検索",
"ko_KR": "찾기",
"no_NO": "Søk",
"pl_PL": "Wyszukaj",
"pt_BR": "Buscar",
"ru_RU": "Поиск",
"sv_SE": "Sök",
"th_TH": "ค้นหา",
"tr_TR": "Ara",
"uk_UA": "Пошук",
"zh_CN": "搜索",
"zh_TW": "搜尋"
}
},
{
"ID": "IrreversibleActionNote",
"Translations": {
"ar_SA": "هذا الإجراء لا يمكن التراجع عنه.",
"de_DE": "Diese Option kann nicht rückgängig gemacht werden.",
"el_GR": "Αυτή η ενέργεια είναι μη αναστρέψιμη.",
"en_US": "This action is not reversible.",
"es_ES": "Esta acción no es reversible.",
"fr_FR": "Cette action n'est pas réversible.",
"he_IL": "הפעולה הזו בלתי הפיכה.",
"it_IT": "Questa azione non è reversibile.",
"ja_JP": "この操作は元に戻せません.",
"ko_KR": "이 작업은 되돌릴 수 없습니다.",
"no_NO": "Denne handlingen er ikke reverserbar.",
"pl_PL": "Ta czynność nie jest odwracalna.",
"pt_BR": "Esta ação não é reversível.",
"ru_RU": "Данное действие является необратимым.",
"sv_SE": "Denna åtgärd går inte att ångra.",
"th_TH": "การดำเนินการนี้ไม่สามารถย้อนกลับได้",
"tr_TR": "Bu eylem geri alınamaz.",
"uk_UA": "Цю дію не можна скасувати.",
"zh_CN": "删除后不可恢复。",
"zh_TW": "此動作將無法復原。"
}
},
{
"ID": "ButtonClose",
"Translations": {
"ar_SA": "إغلاق",
"de_DE": "Schließen",
"el_GR": "Κλείσιμο",
"en_US": "Close",
"es_ES": "Cerrar",
"fr_FR": "Fermer",
"he_IL": "סגירה",
"it_IT": "Chiudi",
"ja_JP": "閉じる",
"ko_KR": "닫기",
"no_NO": "Lukk",
"pl_PL": "Zamknij",
"pt_BR": "Fechar",
"ru_RU": "Закрыть",
"sv_SE": "Stäng",
"th_TH": "ปิด",
"tr_TR": "Kapat",
"uk_UA": "Закрити",
"zh_CN": "关闭",
"zh_TW": "關閉"
}
},
{
"ID": "NameLabel",
"Translations": {
"ar_SA": "الاسم:",
"de_DE": null,
"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": "ProfileNameSelectionWatermark",
"Translations": {
"ar_SA": "اختر اسم الملف الشخصي",
"de_DE": "Wähle einen Profilnamen",
"el_GR": "Επιλέξτε όνομα προφίλ",
"en_US": "Choose a Profile Name",
"es_ES": "Escoge un Nombre de Perfil",
"fr_FR": "Choisir un Nom de Profil",
"he_IL": "בחרו שם פרופיל",
"it_IT": "Scegli un Nome Profilo",
"ja_JP": "プロフィール名を選択",
"ko_KR": "프로필 이름 선택",
"no_NO": "Velg et Profilnavn",
"pl_PL": "Wybierz nazwę profilu",
"pt_BR": "Escolha um Nome de Perfil",
"ru_RU": "Выберите имя профиля",
"sv_SE": "Välj ett Profilnamn",
"th_TH": "เลือก ชื่อโปรไฟล์",
"tr_TR": "Profil Adı Seç",
"uk_UA": "Оберіть ім'я профілю",
"zh_CN": "选择个人资料名称",
"zh_TW": "選擇個人資料名稱"
}
},
{
"ID": "UserIdLabel",
"Translations": {
"ar_SA": "معرف المستخدم:",
"de_DE": "Benutzer-ID:",
"el_GR": "Ταυτότητα Χρήστη:",
"en_US": "User ID:",
"es_ES": "ID de Usuario:",
"fr_FR": "Identifiant Utilisateur :",
"he_IL": "מזהה משתמש:",
"it_IT": "ID utente:",
"ja_JP": "ユーザID:",
"ko_KR": "사용자 ID :",
"no_NO": "Bruker ID:",
"pl_PL": "ID Użytkownika:",
"pt_BR": "ID de Usuário:",
"ru_RU": "ID пользователя:",
"sv_SE": "Användar-id:",
"th_TH": "รหัสผู้ใช้:",
"tr_TR": "Kullanıcı ID:",
"uk_UA": "ID користувача:",
"zh_CN": "用户 ID",
"zh_TW": "使用者 ID:"
}
},
{
"ID": "ProfileImage_Import",
"Translations": {
"ar_SA": "استيراد الصورة",
"de_DE": "Bild importieren",
"el_GR": "Εισαγωγή Εικόνας",
"en_US": "Import Image",
"es_ES": "Importar Imagen",
"fr_FR": "Importer une image",
"he_IL": "ייבוא תמונה",
"it_IT": "Importa immagine",
"ja_JP": "画像をインポート",
"ko_KR": "이미지 가져오기",
"no_NO": "Importer bilde",
"pl_PL": "Importuj obraz",
"pt_BR": "Importar Imagem",
"ru_RU": "Импорт изображения",
"sv_SE": "Importera bild",
"th_TH": "นำเข้าภาพ",
"tr_TR": "Resim İçeri Aktar",
"uk_UA": "Імпорт зображення",
"zh_CN": "导入图像",
"zh_TW": "匯入圖像"
}
},
{
"ID": "ProfileImage_SelectAvatar",
"Translations": {
"ar_SA": "حدد صورة الأفاتار من البرنامج الثابت",
"de_DE": "Firmware-Avatar auswählen",
"el_GR": "Επιλέξτε Avatar από Firmware",
"en_US": "Select Firmware Avatar",
"es_ES": "Seleccionar Avatar del Firmware",
"fr_FR": "Choisir un Avatar du Firmware",
"he_IL": "בחרו אוואטר קושחה",
"it_IT": "Seleziona avatar dal firmware",
"ja_JP": "ファームウェア内のアバターを選択",
"ko_KR": "펌웨어 아바타 선택",
"no_NO": "Velg firmware-avatar",
"pl_PL": "Wybierz avatar z oprogramowania",
"pt_BR": "Selecionar Avatar do Firmware",
"ru_RU": "Выбрать аватар прошивки",
"sv_SE": "Välj avatar från firmware",
"th_TH": "เลือกอวาต้าจากระบบ",
"tr_TR": "Yazılım Avatarı Seç",
"uk_UA": "Виберіть аватар прошивки",
"zh_CN": "选择固件头像",
"zh_TW": "選取韌體大頭貼"
}
},
{
"ID": "SupportedImageFormatDialogTitle",
"Translations": {
"ar_SA": "اختر إما JPG أو JPEG أو PNG أو BMP",
"de_DE": "Wählen Sie entweder ein JPG, JPEG, PNG oder BMP",
"el_GR": "Επιλέξτε είτε JPG, JPEG, PNG ή BMP",
"en_US": "Choose either a JPG, JPEG, PNG, or BMP",
"es_ES": "Elige ya sea JPG, JPEG, PNG o BMP",
"fr_FR": "Choisissez soit un JPG, JPEG, PNG ou BMP",
"he_IL": "בחר את JPG, JPEG, PNG או BMP",
"it_IT": "Scegli tra JPG, JPEG, PNG o BMP",
"ja_JP": "JPG、JPEG、PNG、またはBMPのいずれかを選択してください",
"ko_KR": "JPG, JPEG, PNG 또는 BMP 중에서 선택하세요",
"no_NO": "Velg enten et JPG, JPEG, PNG eller BMP",
"pl_PL": "Wybierz JPG, JPEG, PNG lub BMP",
"pt_BR": "Escolha JPG, JPEG, PNG ou BMP",
"ru_RU": "Выберите либо JPG, JPEG, PNG, или BMP",
"sv_SE": "Välj antingen ett JPG, JPEG, PNG eller BMP",
"th_TH": "เลือก JPG, JPEG, PNG หรือ BMP",
"tr_TR": "JPG, JPEG, PNG veya BMP seçin",
"uk_UA": "Виберіть або JPG, JPEG, PNG, або BMP",
"zh_CN": "选择 JPG、JPEG、PNG 或 BMP",
"zh_TW": "選擇 JPG、JPEG、PNG 或 BMP"
}
},
{
"ID": "SelectAvatarTitle",
"Translations": {
"ar_SA": "تحديد أفاتار البرنامج الثابت",
"de_DE": "Firmware-Avatar auswählen",
"el_GR": "Επιλογή Avatar Firmware",
"en_US": "Select Firmware Avatar",
"es_ES": "Seleccionar Avatar del Firmware",
"fr_FR": "Sélection dun Avatar du Firmware",
"he_IL": "בחירת אוואטר קושחה",
"it_IT": "Selezione Avatar Firmware",
"ja_JP": "ファームウェアアバター選択",
"ko_KR": "펌웨어 아바타 선택",
"no_NO": "Velg firmware-avatar",
"pl_PL": "Wybór awatara oprogramowania",
"pt_BR": "Selecionar Avatar do Firmware",
"ru_RU": "Выбор аватара прошивки",
"sv_SE": "Välj firmware-avatar",
"th_TH": "การเลือกอวตารเฟิร์มแวร์",
"tr_TR": "Firmware Avatar Seçimi",
"uk_UA": "Вибір аватара прошивки",
"zh_CN": "选择固件头像",
"zh_TW": "選取韌體頭像"
}
},
{
"ID": "ButtonChooseAvatar",
"Translations": {
"ar_SA": "اختر الأفاتار",
"de_DE": "Wähle Avatar",
"el_GR": "Επιλέξτε Avatar",
"en_US": "Choose Avatar",
"es_ES": "Elegir Avatar",
"fr_FR": "Choisir un Avatar",
"he_IL": "בחרו אוואטר",
"it_IT": "Scegli Avatar",
"ja_JP": "アバターを選択",
"ko_KR": "아바타 선택",
"no_NO": "Velg avatar",
"pl_PL": "Wybierz awatar",
"pt_BR": "Escolher Avatar",
"ru_RU": "Выбрать аватар",
"sv_SE": "Välj avatar",
"th_TH": "เลือกอวาต้าของคุณ",
"tr_TR": "Avatar Seç",
"uk_UA": "Вибрати аватар",
"zh_CN": "选择头像",
"zh_TW": "選擇大頭貼"
}
},
{
"ID": "DialogUserProfileUnsavedChangesMessage",
"Translations": {
"ar_SA": "لقد قمت بإجراء تغييرات غير محفوظة على هذا الملف الشخصي.",
"de_DE": "Sie haben nicht gespeicherte Änderungen an diesem Profil.",
"el_GR": "Έχετε μη αποθηκευμένες αλλαγές σε αυτό το προφίλ.",
"en_US": "You have unsaved changes to this profile.",
"es_ES": "Tienes cambios no guardados en este perfil.",
"fr_FR": "Vous avez des modifications non enregistrées sur ce profil.",
"he_IL": "ביצעת שינויים לא שמורים בפרופיל זה.",
"it_IT": "Hai modifiche non salvate su questo profilo.",
"ja_JP": "このプロファイルには保存されていない変更があります.",
"ko_KR": "이 프로필에는 저장되지 않은 변경 사항이 있습니다.",
"no_NO": "Du har usparende endringer på denne profilen.",
"pl_PL": "Masz niezapisane zmiany w tym profilu.",
"pt_BR": "Você tem alterações não salvas neste perfil.",
"ru_RU": "У вас есть несохраненные изменения в этом профиле.",
"sv_SE": "Du har osparade ändringar i den här profilen.",
"th_TH": "คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึกในโปรไฟล์นี้",
"tr_TR": "Bu profilde kaydedilmemiş değişiklikleriniz var.",
"uk_UA": "У вас є незбережені зміни в цьому профілі.",
"zh_CN": "您对该账户有未保存的更改。",
"zh_TW": "您對該使用者設定檔有未儲存的變更。"
}
},
{
"ID": "DialogUserProfileUnsavedChangesSubMessage",
"Translations": {
"ar_SA": "هل تريد تجاهل التغييرات؟",
"de_DE": "Verwerfen Sie die Änderungen?",
"el_GR": "Θέλετε να απορρίψετε τις αλλαγές?",
"en_US": "Discard changes?",
"es_ES": "¿Descartar los cambios?",
"fr_FR": "Annuler les modifications ?",
"he_IL": "האם ברצונך להתעלם מהשינויים?",
"it_IT": "Scartare le modifiche?",
"ja_JP": "変更を破棄しますか?",
"ko_KR": "변경 사항을 취소하시겠습니까?",
"no_NO": "Vil du forkaste endringene?",
"pl_PL": "Czy chcesz odrzucić zmiany?",
"pt_BR": "Deseja descartar as alterações?",
"ru_RU": "Отменить изменения?",
"sv_SE": "Vill du förkasta ändringarna?",
"th_TH": "คุณต้องการทิ้งการเปลี่ยนแปลงหรือไม่?",
"tr_TR": "Değişiklikleri iptal et?",
"uk_UA": "Бажаєте скасувати зміни?",
"zh_CN": "确定要放弃更改吗?",
"zh_TW": "您確定要放棄變更嗎?"
}
},
{
"ID": "DialogUserProfileUnsavedChangesTitle",
"Translations": {
"ar_SA": "تحذير - التغييرات غير محفوظة",
"de_DE": "WARNUNG - Nicht gespeicherte Änderungen",
"el_GR": "ΠΡΟΣΟΧΗ - Μην Αποθηκευμένες Αλλαγές.",
"en_US": "WARNING - Unsaved Changes",
"es_ES": "ADVERTENCIA - Cambios Sin Guardar",
"fr_FR": "AVERTISSEMENT - Modifications Non Enregistrées",
"he_IL": "אזהרה - שינויים לא שמורים",
"it_IT": "ATTENZIONE - Modifiche non salvate",
"ja_JP": "警告 - 保存されていない変更",
"ko_KR": "경고 - 저장되지 않은 변경 사항",
"no_NO": "ADVARSEL - Ulagrede endringer",
"pl_PL": "UWAGA - Niezapisane zmiany",
"pt_BR": "ALERTA - Alterações não salvas",
"ru_RU": "ВНИМАНИЕ - Несохраненные изменения",
"sv_SE": "VARNING - Ej sparade ändringar",
"th_TH": "คำเตือน - มีการเปลี่ยนแปลงที่ไม่ได้บันทึก",
"tr_TR": "UYARI - Kaydedilmemiş Değişiklikler",
"uk_UA": "УВАГА — Незбережені зміни",
"zh_CN": "警告 - 有未保存的更改",
"zh_TW": "警告 - 未儲存的變更"
}
},
{
"ID": "DialogUserProfileDeletionConfirmMessage",
"Translations": {
"ar_SA": "هل حذف الملف الشخصي المحدد؟",
"de_DE": "Löschen Sie das ausgewählte Profil?",
"el_GR": "Διαγραφή του επιλεγμένου προφίλ;",
"en_US": "Delete the selected profile?",
"es_ES": "¿Eliminar el perfil seleccionado?",
"fr_FR": "Supprimer le profil sélectionné ?",
"he_IL": "האם למחוק את הפרופיל שנבחר?",
"it_IT": "Eliminare il profilo selezionato?",
"ja_JP": "選択されたプロファイルを削除しますか?",
"ko_KR": "선택한 프로필을 삭제하시겠습니까?",
"no_NO": "Slette den valgte profilen?",
"pl_PL": "Usunąć wybrany profil?",
"pt_BR": "Excluir o perfil selecionado?",
"ru_RU": "Удалить выбранный профиль?",
"sv_SE": "Ta bort den valda profilen?",
"th_TH": "ลบโปรไฟล์ที่เลือก?",
"tr_TR": "Seçilen profili silmek?",
"uk_UA": "Видалити вибраний профіль?",
"zh_CN": "删除所选账户?",
"zh_TW": "刪除所選設定檔?"
}
},
{
"ID": "DialogUserProfileDeletionWarningMessage",
"Translations": {
"ar_SA": "لن تكون هناك ملفات الشخصية أخرى لفتحها إذا تم حذف الملف الشخصي المحدد.",
"de_DE": "Es können keine anderen Profile geöffnet werden, wenn das ausgewählte Profil gelöscht wird.",
"el_GR": "Δεν θα υπάρχουν άλλα προφίλ εάν διαγραφεί το επιλεγμένο.",
"en_US": "There would be no other profiles to be opened if selected profile is deleted.",
"es_ES": "Si eliminas el perfil seleccionado no quedará ningún otro perfil.",
"fr_FR": "Il n'y aurait aucun autre profil à ouvrir si le profil sélectionné est supprimé.",
"he_IL": "לא יהיו פרופילים אחרים שייפתחו אם הפרופיל שנבחר יימחק.",
"it_IT": "Non ci sarebbero altri profili da aprire se il profilo selezionato venisse cancellato.",
"ja_JP": "選択されたプロファイルを削除すると,プロファイルがひとつも存在しなくなります.",
"ko_KR": "선택한 프로필을 삭제하면 다른 프로필을 열 수 없음.",
"no_NO": "Det vil ikke være noen profiler å åpnes hvis valgt profil blir slettet.",
"pl_PL": "Nie będzie innych profili do otwarcia, jeśli wybrany profil zostanie usunięty.",
"pt_BR": "Não haveria nenhum perfil selecionado se o perfil atual fosse deletado.",
"ru_RU": "Если выбранный профиль будет удален, другие профили не будут открываться.",
"sv_SE": "Det skulle inte finnas några andra profiler att öppnas om angiven profil tas bort.",
"th_TH": "จะไม่มีโปรไฟล์อื่นให้เปิดหากโปรไฟล์ที่เลือกถูกลบ",
"tr_TR": "Seçilen profil silinirse kullanılabilen başka profil kalmayacak.",
"uk_UA": "Якщо вибраний профіль буде видалено, інші профілі не відкриватимуться.",
"zh_CN": "删除后将没有可用的账户。",
"zh_TW": "如果刪除選取的設定檔,將無法開啟其他設定檔。"
}
},
{
"ID": "ButtonDelete",
"Translations": {
"ar_SA": "حذف",
"de_DE": "Löschen",
"el_GR": "Διαγράφω",
"en_US": "Delete",
"es_ES": "Eliminar",
"fr_FR": "Supprimer",
"he_IL": "מחיקה",
"it_IT": "Elimina",
"ja_JP": "削除",
"ko_KR": "삭제",
"no_NO": "Slett",
"pl_PL": "Usuń",
"pt_BR": "Apagar",
"ru_RU": "Удалить",
"sv_SE": "Ta bort",
"th_TH": "ลบ",
"tr_TR": "Sil",
"uk_UA": "Видалити",
"zh_CN": "删除",
"zh_TW": "刪除"
}
},
{
"ID": "ButtonSave",
"Translations": {
"ar_SA": "حفظ",
"de_DE": "Speichern",
"el_GR": "Αποθήκευση",
"en_US": "Save",
"es_ES": "Guardar",
"fr_FR": "Enregistrer",
"he_IL": "שמור",
"it_IT": "Salva",
"ja_JP": "セーブ",
"ko_KR": "저장",
"no_NO": "Lagre",
"pl_PL": "Zapisz",
"pt_BR": "Salvar",
"ru_RU": "Сохранить",
"sv_SE": "Spara",
"th_TH": "บันทึก",
"tr_TR": "Kaydet",
"uk_UA": "Зберегти",
"zh_CN": "保存",
"zh_TW": "儲存"
}
},
{
"ID": "UserEditorTitle",
"Translations": {
"ar_SA": "جارٍ تعديل {0}",
"de_DE": "{0} wird bearbeitet",
"el_GR": "Επεξεργασία {0}",
"en_US": "Editing {0}",
"es_ES": "Editando {0}",
"fr_FR": "Modification de {0}",
"he_IL": "עריכת {0}",
"it_IT": "Modifica di {0}",
"ja_JP": "{0} を編集中",
"ko_KR": "{0} 편집 중",
"no_NO": "Redigerer {0}",
"pl_PL": "Edycja {0}",
"pt_BR": "Editando {0}",
"ru_RU": "Редактирование {0}",
"sv_SE": "Redigerar {0}",
"th_TH": "กำลังกำลังแก้ไข {0}",
"tr_TR": "{0} düzenleniyor",
"uk_UA": "Редагування {0}",
"zh_CN": "正在编辑 {0}",
"zh_TW": "正在編輯 {0}"
}
},
{
"ID": "UserEditorTitleNewUser",
"Translations": {
"ar_SA": "مستخدم جديد",
"de_DE": "Neuer Nutzer",
"el_GR": "Νέος Χρήστης",
"en_US": "New User",
"es_ES": "Nuevo Usuario",
"fr_FR": "Nouvel Utilisateur",
"he_IL": "משתמש חדש",
"it_IT": "Nuovo utente",
"ja_JP": "新しいユーザー",
"ko_KR": "새 사용자",
"no_NO": "Ny bruker",
"pl_PL": "Nowy użytkownik",
"pt_BR": "Novo usuário",
"ru_RU": "Новый пользователь",
"sv_SE": "Ny användare",
"th_TH": "ผู้ใช้ใหม่",
"tr_TR": "Yeni kullanıcı",
"uk_UA": "Новий користувач",
"zh_CN": "新用户",
"zh_TW": "新使用者"
}
},
{
"ID": "EmptyNameError",
"Translations": {
"ar_SA": "الاسم مطلوب",
"de_DE": "Name ist erforderlich",
"el_GR": "Απαιτείται όνομα",
"en_US": "Name is required",
"es_ES": "El nombre es obligatorio",
"fr_FR": "Le nom est requis",
"he_IL": "נדרש שם",
"it_IT": "Il nome è obbligatorio",
"ja_JP": "名称が必要です",
"ko_KR": "이름 필수 입력",
"no_NO": "Navn er påkrevd",
"pl_PL": "Nazwa jest wymagana",
"pt_BR": "Nome é obrigatório",
"ru_RU": "Необходимо ввести имя",
"sv_SE": "Namn krävs",
"th_TH": "จำเป็นต้องระบุชื่อ",
"tr_TR": "İsim gerekli",
"uk_UA": "Імʼя обовʼязкове",
"zh_CN": "必须输入名称",
"zh_TW": "名稱為必填"
}
}
]
}

View File

@@ -168,7 +168,7 @@ namespace ARMeilleure.Common
{ {
_allocated.Dispose(); _allocated.Dispose();
foreach (nint page in _pages.Values) foreach (IntPtr page in _pages.Values)
{ {
NativeAllocator.Instance.Free((void*)page); NativeAllocator.Instance.Free((void*)page);
} }

View File

@@ -1,16 +0,0 @@
namespace Ryujinx.Audio.Backends.Apple
{
class AppleAudioBuffer
{
public readonly ulong DriverIdentifier;
public readonly ulong SampleCount;
public ulong SamplePlayed;
public AppleAudioBuffer(ulong driverIdentifier, ulong sampleCount)
{
DriverIdentifier = driverIdentifier;
SampleCount = sampleCount;
SamplePlayed = 0;
}
}
}

View File

@@ -1,196 +0,0 @@
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using System.Runtime.Versioning;
using Ryujinx.Audio.Backends.Apple.Native;
using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox;
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
namespace Ryujinx.Audio.Backends.Apple
{
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("ios")]
public sealed class AppleHardwareDeviceDriver : IHardwareDeviceDriver
{
private readonly ManualResetEvent _updateRequiredEvent;
private readonly ManualResetEvent _pauseEvent;
private readonly ConcurrentDictionary<AppleHardwareDeviceSession, byte> _sessions;
private readonly bool _supportSurroundConfiguration;
public float Volume { get; set; }
public AppleHardwareDeviceDriver()
{
_updateRequiredEvent = new ManualResetEvent(false);
_pauseEvent = new ManualResetEvent(true);
_sessions = new ConcurrentDictionary<AppleHardwareDeviceSession, byte>();
_supportSurroundConfiguration = TestSurroundSupport();
Volume = 1f;
}
private bool TestSurroundSupport()
{
try
{
AudioStreamBasicDescription format =
GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6);
int result = AudioQueueNewOutput(
ref format,
nint.Zero,
nint.Zero,
nint.Zero,
nint.Zero,
0,
out nint testQueue);
if (result == 0)
{
AudioChannelLayout layout = new AudioChannelLayout
{
AudioChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_A,
AudioChannelBitmap = 0,
NumberChannelDescriptions = 0
};
int layoutResult = AudioQueueSetProperty(
testQueue,
kAudioQueueProperty_ChannelLayout,
ref layout,
(uint)Marshal.SizeOf<AudioChannelLayout>());
if (layoutResult == 0)
{
AudioQueueDispose(testQueue, true);
return true;
}
AudioQueueDispose(testQueue, true);
}
return false;
}
catch
{
return false;
}
}
public static bool IsSupported => OperatingSystem.IsMacOSVersionAtLeast(10, 5);
public ManualResetEvent GetUpdateRequiredEvent()
=> _updateRequiredEvent;
public ManualResetEvent GetPauseEvent()
=> _pauseEvent;
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager,
SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
channelCount = 2;
}
if (sampleRate == 0)
{
sampleRate = Constants.TargetSampleRate;
}
if (direction != Direction.Output)
{
throw new NotImplementedException("Input direction is currently not implemented on Apple backend!");
}
AppleHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);
return session;
}
internal bool Unregister(AppleHardwareDeviceSession session)
=> _sessions.TryRemove(session, out _);
internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate,
uint channelCount)
{
uint formatFlags;
uint bitsPerChannel;
switch (sampleFormat)
{
case SampleFormat.PcmInt8:
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
bitsPerChannel = 8;
break;
case SampleFormat.PcmInt16:
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
bitsPerChannel = 16;
break;
case SampleFormat.PcmInt32:
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
bitsPerChannel = 32;
break;
case SampleFormat.PcmFloat:
formatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
bitsPerChannel = 32;
break;
default:
throw new ArgumentException($"Unsupported sample format {sampleFormat}");
}
uint bytesPerFrame = (bitsPerChannel / 8) * channelCount;
return new AudioStreamBasicDescription
{
SampleRate = sampleRate,
FormatID = kAudioFormatLinearPCM,
FormatFlags = formatFlags,
BytesPerPacket = bytesPerFrame,
FramesPerPacket = 1,
BytesPerFrame = bytesPerFrame,
ChannelsPerFrame = channelCount,
BitsPerChannel = bitsPerChannel,
Reserved = 0
};
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
private void Dispose(bool disposing)
{
if (disposing)
{
foreach (AppleHardwareDeviceSession session in _sessions.Keys)
{
session.Dispose();
}
_pauseEvent.Dispose();
}
}
public bool SupportsDirection(Direction direction)
=> direction != Direction.Input;
public bool SupportsSampleRate(uint sampleRate) => true;
public bool SupportsSampleFormat(SampleFormat sampleFormat)
=> sampleFormat != SampleFormat.PcmInt24;
public bool SupportsChannelCount(uint channelCount)
=> channelCount != 6 || _supportSurroundConfiguration;
}
}

View File

@@ -1,285 +0,0 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Memory;
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using System.Runtime.Versioning;
using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox;
namespace Ryujinx.Audio.Backends.Apple
{
[SupportedOSPlatform("macos")]
[SupportedOSPlatform("ios")]
class AppleHardwareDeviceSession : HardwareDeviceSessionOutputBase
{
private const int NumBuffers = 3;
private readonly AppleHardwareDeviceDriver _driver;
private readonly ConcurrentQueue<AppleAudioBuffer> _queuedBuffers = new();
private readonly DynamicRingBuffer _ringBuffer = new();
private readonly ManualResetEvent _updateRequiredEvent;
private readonly AudioQueueOutputCallback _callbackDelegate;
private readonly GCHandle _gcHandle;
private nint _audioQueue;
private readonly nint[] _audioQueueBuffers = new nint[NumBuffers];
private readonly int[] _bufferBytesFilled = new int[NumBuffers];
private readonly int _bytesPerFrame;
private ulong _playedSampleCount;
private bool _started;
private float _volume = 1f;
private readonly object _lock = new();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void AudioQueueOutputCallback(
nint userData,
nint audioQueue,
nint buffer);
public AppleHardwareDeviceSession(
AppleHardwareDeviceDriver driver,
IVirtualMemoryManager memoryManager,
SampleFormat requestedSampleFormat,
uint requestedSampleRate,
uint requestedChannelCount)
: base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_updateRequiredEvent = driver.GetUpdateRequiredEvent();
_callbackDelegate = OutputCallback;
_bytesPerFrame = BackendHelper.GetSampleSize(requestedSampleFormat) * (int)requestedChannelCount;
_gcHandle = GCHandle.Alloc(this, GCHandleType.Normal);
SetupAudioQueue();
}
private void SetupAudioQueue()
{
lock (_lock)
{
AudioStreamBasicDescription format = AppleHardwareDeviceDriver.GetAudioFormat(
RequestedSampleFormat,
RequestedSampleRate,
RequestedChannelCount);
nint callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate);
nint userData = GCHandle.ToIntPtr(_gcHandle);
int result = AudioQueueNewOutput(
ref format,
callbackPtr,
userData,
nint.Zero,
nint.Zero,
0,
out _audioQueue);
if (result != 0)
{
throw new InvalidOperationException($"AudioQueueNewOutput failed: {result}");
}
uint framesPerBuffer = RequestedSampleRate / 100;
uint bufferSize = framesPerBuffer * (uint)_bytesPerFrame;
for (int i = 0; i < NumBuffers; i++)
{
AudioQueueAllocateBuffer(_audioQueue, bufferSize, out _audioQueueBuffers[i]);
_bufferBytesFilled[i] = 0;
PrimeBuffer(_audioQueueBuffers[i], i);
}
}
}
private unsafe void PrimeBuffer(nint bufferPtr, int bufferIndex)
{
AudioQueueBuffer* buffer = (AudioQueueBuffer*)bufferPtr;
int capacityBytes = (int)buffer->AudioDataBytesCapacity;
int framesPerBuffer = capacityBytes / _bytesPerFrame;
int availableFrames = _ringBuffer.Length / _bytesPerFrame;
int framesToRead = Math.Min(availableFrames, framesPerBuffer);
int bytesToRead = framesToRead * _bytesPerFrame;
Span<byte> dst = new((void*)buffer->AudioData, capacityBytes);
dst.Clear();
if (bytesToRead > 0)
{
Span<byte> audio = dst.Slice(0, bytesToRead);
_ringBuffer.Read(audio, 0, bytesToRead);
ApplyVolume(buffer->AudioData, bytesToRead);
}
buffer->AudioDataByteSize = (uint)capacityBytes;
_bufferBytesFilled[bufferIndex] = bytesToRead;
AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, nint.Zero);
}
private void OutputCallback(nint userData, nint audioQueue, nint bufferPtr)
{
if (!_started || bufferPtr == nint.Zero)
return;
int bufferIndex = Array.IndexOf(_audioQueueBuffers, bufferPtr);
if (bufferIndex < 0)
return;
int bytesPlayed = _bufferBytesFilled[bufferIndex];
if (bytesPlayed > 0)
{
ProcessPlayedSamples(bytesPlayed);
}
PrimeBuffer(bufferPtr, bufferIndex);
}
private void ProcessPlayedSamples(int bytesPlayed)
{
ulong samplesPlayed = GetSampleCount(bytesPlayed);
ulong remaining = samplesPlayed;
bool needUpdate = false;
while (remaining > 0 && _queuedBuffers.TryPeek(out AppleAudioBuffer buffer))
{
ulong needed = buffer.SampleCount - Interlocked.Read(ref buffer.SamplePlayed);
ulong take = Math.Min(needed, remaining);
ulong played = Interlocked.Add(ref buffer.SamplePlayed, take);
remaining -= take;
if (played == buffer.SampleCount)
{
_queuedBuffers.TryDequeue(out _);
needUpdate = true;
}
Interlocked.Add(ref _playedSampleCount, take);
}
if (needUpdate)
{
_updateRequiredEvent.Set();
}
}
private unsafe void ApplyVolume(nint dataPtr, int byteSize)
{
float volume = Math.Clamp(_volume * _driver.Volume, 0f, 1f);
if (volume >= 0.999f)
return;
int sampleCount = byteSize / BackendHelper.GetSampleSize(RequestedSampleFormat);
switch (RequestedSampleFormat)
{
case SampleFormat.PcmInt16:
short* s16 = (short*)dataPtr;
for (int i = 0; i < sampleCount; i++)
s16[i] = (short)(s16[i] * volume);
break;
case SampleFormat.PcmFloat:
float* f32 = (float*)dataPtr;
for (int i = 0; i < sampleCount; i++)
f32[i] *= volume;
break;
case SampleFormat.PcmInt32:
int* s32 = (int*)dataPtr;
for (int i = 0; i < sampleCount; i++)
s32[i] = (int)(s32[i] * volume);
break;
case SampleFormat.PcmInt8:
sbyte* s8 = (sbyte*)dataPtr;
for (int i = 0; i < sampleCount; i++)
s8[i] = (sbyte)(s8[i] * volume);
break;
}
}
public override void QueueBuffer(AudioBuffer buffer)
{
_ringBuffer.Write(buffer.Data, 0, buffer.Data.Length);
_queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.DataPointer, GetSampleCount(buffer)));
}
public override void Start()
{
lock (_lock)
{
if (_started)
return;
_started = true;
AudioQueueStart(_audioQueue, nint.Zero);
}
}
public override void Stop()
{
lock (_lock)
{
if (!_started)
return;
_started = false;
AudioQueuePause(_audioQueue);
}
}
public override ulong GetPlayedSampleCount()
=> Interlocked.Read(ref _playedSampleCount);
public override float GetVolume() => _volume;
public override void SetVolume(float volume) => _volume = volume;
public override bool WasBufferFullyConsumed(AudioBuffer buffer)
{
if (!_queuedBuffers.TryPeek(out AppleAudioBuffer driverBuffer))
return true;
return driverBuffer.DriverIdentifier != buffer.DataPointer;
}
public override void PrepareToClose() { }
public override void UnregisterBuffer(AudioBuffer buffer) { }
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Stop();
if (_audioQueue != nint.Zero)
{
AudioQueueStop(_audioQueue, true);
AudioQueueDispose(_audioQueue, true);
_audioQueue = nint.Zero;
}
if (_gcHandle.IsAllocated)
{
_gcHandle.Free();
}
}
}
public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -1,102 +0,0 @@
using System.Runtime.InteropServices;
// ReSharper disable InconsistentNaming
namespace Ryujinx.Audio.Backends.Apple.Native
{
public static partial class AudioToolbox
{
[StructLayout(LayoutKind.Sequential)]
internal struct AudioStreamBasicDescription
{
public double SampleRate;
public uint FormatID;
public uint FormatFlags;
public uint BytesPerPacket;
public uint FramesPerPacket;
public uint BytesPerFrame;
public uint ChannelsPerFrame;
public uint BitsPerChannel;
public uint Reserved;
}
[StructLayout(LayoutKind.Sequential)]
internal struct AudioChannelLayout
{
public uint AudioChannelLayoutTag;
public uint AudioChannelBitmap;
public uint NumberChannelDescriptions;
}
internal const uint kAudioFormatLinearPCM = 0x6C70636D;
internal const uint kAudioQueueProperty_ChannelLayout = 0x6171636c;
internal const uint kAudioChannelLayoutTag_MPEG_5_1_A = 0x650006;
internal const uint kAudioFormatFlagIsFloat = (1 << 0);
internal const uint kAudioFormatFlagIsSignedInteger = (1 << 2);
internal const uint kAudioFormatFlagIsPacked = (1 << 3);
internal const uint kAudioFormatFlagIsBigEndian = (1 << 1);
internal const uint kAudioFormatFlagIsAlignedHigh = (1 << 4);
internal const uint kAudioFormatFlagIsNonInterleaved = (1 << 5);
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
internal static partial int AudioQueueNewOutput(
ref AudioStreamBasicDescription format,
nint callback,
nint userData,
nint callbackRunLoop,
nint callbackRunLoopMode,
uint flags,
out nint audioQueue);
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
internal static partial int AudioQueueSetProperty(
nint audioQueue,
uint propertyID,
ref AudioChannelLayout layout,
uint layoutSize);
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
internal static partial int AudioQueueDispose(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate);
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
internal static partial int AudioQueueAllocateBuffer(
nint audioQueue,
uint bufferByteSize,
out nint buffer);
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
internal static partial int AudioQueueStart(nint audioQueue, nint startTime);
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
internal static partial int AudioQueuePause(nint audioQueue);
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
internal static partial int AudioQueueStop(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate);
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
internal static partial int AudioQueueSetParameter(
nint audioQueue,
uint parameterID,
float value);
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
internal static partial int AudioQueueEnqueueBuffer(
nint audioQueue,
nint buffer,
uint numPacketDescs,
nint packetDescs);
[StructLayout(LayoutKind.Sequential)]
internal struct AudioQueueBuffer
{
public uint AudioDataBytesCapacity;
public nint AudioData;
public uint AudioDataByteSize;
public nint UserData;
public uint PacketDescriptionCapacity;
public nint PacketDescriptions;
public uint PacketDescriptionCount;
}
internal const uint kAudioQueueParam_Volume = 1;
}
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -10,8 +10,7 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
namespace Ryujinx.Audio.Backends.OpenAL namespace Ryujinx.Audio.Backends.OpenAL
{ {
// ReSharper disable once InconsistentNaming public class OpenALHardwareDeviceDriver : IHardwareDeviceDriver
public sealed class OpenALHardwareDeviceDriver : IHardwareDeviceDriver
{ {
private readonly ALDevice _device; private readonly ALDevice _device;
private readonly ALContext _context; private readonly ALContext _context;
@@ -149,7 +148,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
Dispose(true); Dispose(true);
} }
private void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (disposing) if (disposing)
{ {

View File

@@ -9,8 +9,7 @@ using System.Threading;
namespace Ryujinx.Audio.Backends.OpenAL namespace Ryujinx.Audio.Backends.OpenAL
{ {
// ReSharper disable once InconsistentNaming class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase
sealed class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase
{ {
private readonly OpenALHardwareDeviceDriver _driver; private readonly OpenALHardwareDeviceDriver _driver;
private readonly int _sourceId; private readonly int _sourceId;
@@ -191,7 +190,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
} }
} }
private void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (disposing && _driver.Unregister(this)) if (disposing && _driver.Unregister(this))
{ {

View File

@@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Backends.SDL3
using unsafe SDL_AudioStreamCallbackPointer = delegate* unmanaged[Cdecl]<nint, SDL_AudioStream*, int, int, void>; using unsafe SDL_AudioStreamCallbackPointer = delegate* unmanaged[Cdecl]<nint, SDL_AudioStream*, int, int, void>;
public sealed class SDL3HardwareDeviceDriver : IHardwareDeviceDriver public class SDL3HardwareDeviceDriver : IHardwareDeviceDriver
{ {
private readonly ManualResetEvent _updateRequiredEvent; private readonly ManualResetEvent _updateRequiredEvent;
private readonly ManualResetEvent _pauseEvent; private readonly ManualResetEvent _pauseEvent;
@@ -162,7 +162,7 @@ namespace Ryujinx.Audio.Backends.SDL3
Dispose(true); Dispose(true);
} }
private void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (disposing) if (disposing)
{ {

View File

@@ -12,7 +12,10 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Backends.SDL3 namespace Ryujinx.Audio.Backends.SDL3
{ {
sealed unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase
unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase
{ {
private readonly SDL3HardwareDeviceDriver _driver; private readonly SDL3HardwareDeviceDriver _driver;
private readonly ConcurrentQueue<SDL3AudioBuffer> _queuedBuffers; private readonly ConcurrentQueue<SDL3AudioBuffer> _queuedBuffers;
@@ -223,7 +226,7 @@ namespace Ryujinx.Audio.Backends.SDL3
return driverBuffer.DriverIdentifier != buffer.DataPointer; return driverBuffer.DriverIdentifier != buffer.DataPointer;
} }
private void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (disposing && _driver.Unregister(this)) if (disposing && _driver.Unregister(this))
{ {

View File

@@ -130,7 +130,7 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native
unsafe unsafe
{ {
int* frameCountPtr = &nativeFrameCount; int* frameCountPtr = &nativeFrameCount;
nint* arenasPtr = &arenas; IntPtr* arenasPtr = &arenas;
CheckError(soundio_outstream_begin_write(_context, (nint)arenasPtr, (nint)frameCountPtr)); CheckError(soundio_outstream_begin_write(_context, (nint)arenasPtr, (nint)frameCountPtr));
frameCount = *frameCountPtr; frameCount = *frameCountPtr;

View File

@@ -10,7 +10,7 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
namespace Ryujinx.Audio.Backends.SoundIo namespace Ryujinx.Audio.Backends.SoundIo
{ {
public sealed class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver public class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver
{ {
private readonly SoundIoContext _audioContext; private readonly SoundIoContext _audioContext;
private readonly SoundIoDeviceContext _audioDevice; private readonly SoundIoDeviceContext _audioDevice;
@@ -227,7 +227,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
} }
} }
private void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (disposing) if (disposing)
{ {

View File

@@ -11,7 +11,7 @@ using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo;
namespace Ryujinx.Audio.Backends.SoundIo namespace Ryujinx.Audio.Backends.SoundIo
{ {
sealed class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase
{ {
private readonly SoundIoHardwareDeviceDriver _driver; private readonly SoundIoHardwareDeviceDriver _driver;
private readonly ConcurrentQueue<SoundIoAudioBuffer> _queuedBuffers; private readonly ConcurrentQueue<SoundIoAudioBuffer> _queuedBuffers;
@@ -428,7 +428,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
} }
} }
private void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (disposing && _driver.Unregister(this)) if (disposing && _driver.Unregister(this))
{ {

View File

@@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Renderer.Common
public uint MixesSize; public uint MixesSize;
public uint SinksSize; public uint SinksSize;
public uint PerformanceBufferSize; public uint PerformanceBufferSize;
public uint SplitterSize; public uint Unknown24;
public uint RenderInfoSize; public uint RenderInfoSize;
#pragma warning disable IDE0051, CS0169 // Remove unused field #pragma warning disable IDE0051, CS0169 // Remove unused field

View File

@@ -433,12 +433,8 @@ namespace Ryujinx.Audio.Renderer.Server
public ResultCode UpdateSplitter(SplitterContext context) public ResultCode UpdateSplitter(SplitterContext context)
{ {
long initialInputConsumed = _inputReader.Consumed;
if (context.Update(ref _inputReader)) if (context.Update(ref _inputReader))
{ {
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.SplitterSize);
return ResultCode.Success; return ResultCode.Success;
} }

View File

@@ -12,6 +12,8 @@ namespace Ryujinx.Common.Logging
{ {
public static class Logger public static class Logger
{ {
public static readonly TextWriter WriterProxy = new TextWriterProxy();
private static readonly Stopwatch _time; private static readonly Stopwatch _time;
private static readonly bool[] _enabledClasses; private static readonly bool[] _enabledClasses;

View File

@@ -0,0 +1,21 @@
using System;
using System.IO;
using System.Text;
namespace Ryujinx.Common.Logging
{
internal class TextWriterProxy : TextWriter
{
public override Encoding Encoding => Console.OutputEncoding;
public override void Write(string value)
{
if (value is null) return;
foreach (var line in value.Split(Console.Out.NewLine))
{
Logger.Info?.PrintMsg(LogClass.Application, line);
}
}
}
}

View File

@@ -30,9 +30,9 @@ namespace ARMeilleure.Common
/// <summary> /// <summary>
/// Base address for the page. /// Base address for the page.
/// </summary> /// </summary>
public readonly nint Address; public readonly IntPtr Address;
public AddressTablePage(bool isSparse, nint address) public AddressTablePage(bool isSparse, IntPtr address)
{ {
IsSparse = isSparse; IsSparse = isSparse;
Address = address; Address = address;
@@ -47,20 +47,20 @@ namespace ARMeilleure.Common
public readonly SparseMemoryBlock Block; public readonly SparseMemoryBlock Block;
private readonly TrackingEventDelegate _trackingEvent; private readonly TrackingEventDelegate _trackingEvent;
public TableSparseBlock(ulong size, Action<nint> ensureMapped, PageInitDelegate pageInit) public TableSparseBlock(ulong size, Action<IntPtr> ensureMapped, PageInitDelegate pageInit)
{ {
SparseMemoryBlock block = new(size, pageInit, null); SparseMemoryBlock block = new(size, pageInit, null);
_trackingEvent = (address, size, write) => _trackingEvent = (address, size, write) =>
{ {
ulong pointer = (ulong)block.Block.Pointer + address; ulong pointer = (ulong)block.Block.Pointer + address;
ensureMapped((nint)pointer); ensureMapped((IntPtr)pointer);
return pointer; return pointer;
}; };
bool added = NativeSignalHandler.AddTrackedRegion( bool added = NativeSignalHandler.AddTrackedRegion(
(nuint)block.Block.Pointer, (nuint)block.Block.Pointer,
(nuint)(block.Block.Pointer + (nint)block.Block.Size), (nuint)(block.Block.Pointer + (IntPtr)block.Block.Size),
Marshal.GetFunctionPointerForDelegate(_trackingEvent)); Marshal.GetFunctionPointerForDelegate(_trackingEvent));
if (!added) if (!added)
@@ -116,7 +116,7 @@ namespace ARMeilleure.Common
} }
/// <inheritdoc/> /// <inheritdoc/>
public nint Base public IntPtr Base
{ {
get get
{ {
@@ -124,7 +124,7 @@ namespace ARMeilleure.Common
lock (_pages) lock (_pages)
{ {
return (nint)GetRootPage(); return (IntPtr)GetRootPage();
} }
} }
} }
@@ -240,7 +240,7 @@ namespace ARMeilleure.Common
long index = Levels[^1].GetValue(address); long index = Levels[^1].GetValue(address);
EnsureMapped((nint)(page + index)); EnsureMapped((IntPtr)(page + index));
return ref page[index]; return ref page[index];
} }
@@ -284,7 +284,7 @@ namespace ARMeilleure.Common
/// Ensure the given pointer is mapped in any overlapping sparse reservations. /// Ensure the given pointer is mapped in any overlapping sparse reservations.
/// </summary> /// </summary>
/// <param name="ptr">Pointer to be mapped</param> /// <param name="ptr">Pointer to be mapped</param>
private void EnsureMapped(nint ptr) private void EnsureMapped(IntPtr ptr)
{ {
if (Sparse) if (Sparse)
{ {
@@ -299,7 +299,7 @@ namespace ARMeilleure.Common
{ {
SparseMemoryBlock sparse = reserved.Block; SparseMemoryBlock sparse = reserved.Block;
if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (nint)sparse.Block.Size) if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size)
{ {
sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer)); sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer));
@@ -319,15 +319,15 @@ namespace ARMeilleure.Common
/// </summary> /// </summary>
/// <param name="level">Level to get the fill value for</param> /// <param name="level">Level to get the fill value for</param>
/// <returns>The fill value</returns> /// <returns>The fill value</returns>
private nint GetFillValue(int level) private IntPtr GetFillValue(int level)
{ {
if (_fillBottomLevel != null && level == Levels.Length - 2) if (_fillBottomLevel != null && level == Levels.Length - 2)
{ {
return (nint)_fillBottomLevelPtr; return (IntPtr)_fillBottomLevelPtr;
} }
else else
{ {
return nint.Zero; return IntPtr.Zero;
} }
} }
@@ -379,7 +379,7 @@ namespace ARMeilleure.Common
/// <param name="fill">Fill value</param> /// <param name="fill">Fill value</param>
/// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param> /// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param>
/// <returns>Allocated block</returns> /// <returns>Allocated block</returns>
private nint Allocate<T>(int length, T fill, bool leaf) where T : unmanaged private IntPtr Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
{ {
int size = sizeof(T) * length; int size = sizeof(T) * length;
@@ -405,7 +405,7 @@ namespace ARMeilleure.Common
} }
} }
page = new AddressTablePage(true, block.Block.Pointer + (nint)_sparseReservedOffset); page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset);
_sparseReservedOffset += (ulong)size; _sparseReservedOffset += (ulong)size;
@@ -413,7 +413,7 @@ namespace ARMeilleure.Common
} }
else else
{ {
nint address = (nint)NativeAllocator.Instance.Allocate((uint)size); IntPtr address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size);
page = new AddressTablePage(false, address); page = new AddressTablePage(false, address);
Span<T> span = new((void*)page.Address, length); Span<T> span = new((void*)page.Address, length);

View File

@@ -658,7 +658,7 @@ namespace Ryujinx.Graphics.Gpu.Image
bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel; bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel;
nint hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0; IntPtr hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size)) if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size))
{ {

View File

@@ -19,7 +19,7 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
public static void Initialize() public static void Initialize()
{ {
nint configSize = (nint)Marshal.SizeOf<MVKConfiguration>(); IntPtr configSize = (nint)Marshal.SizeOf<MVKConfiguration>();
vkGetMoltenVKConfigurationMVK(nint.Zero, out MVKConfiguration config, configSize); vkGetMoltenVKConfigurationMVK(nint.Zero, out MVKConfiguration config, configSize);

View File

@@ -86,7 +86,7 @@ namespace Ryujinx.Graphics.Vulkan
enabledExtensions = enabledExtensions.Append(ExtDebugUtils.ExtensionName).ToArray(); enabledExtensions = enabledExtensions.Append(ExtDebugUtils.ExtensionName).ToArray();
} }
nint appName = Marshal.StringToHGlobalAnsi(AppName); IntPtr appName = Marshal.StringToHGlobalAnsi(AppName);
ApplicationInfo applicationInfo = new() ApplicationInfo applicationInfo = new()
{ {
@@ -166,7 +166,7 @@ namespace Ryujinx.Graphics.Vulkan
internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api) internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api)
{ {
nint appName = Marshal.StringToHGlobalAnsi(AppName); IntPtr appName = Marshal.StringToHGlobalAnsi(AppName);
ApplicationInfo applicationInfo = new() ApplicationInfo applicationInfo = new()
{ {

View File

@@ -5,7 +5,6 @@ using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.Types;
@@ -15,7 +14,6 @@ using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
using Ryujinx.Memory; using Ryujinx.Memory;
using System; using System;
using System.ComponentModel;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
@@ -489,23 +487,6 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
return ResultCode.Success; return ResultCode.Success;
} }
[CommandCmif(106)] // 20.0.0+
// SetProtocol
public ResultCode SetProtocol(ServiceCtx context)
{
uint protocolValue = context.RequestData.ReadUInt32();
// On NX only input value 1 or 3 is allowed, with an error being thrown otherwise.
if (protocolValue != 1 && protocolValue != 3)
{
throw new ArgumentException($"{GetType().FullName}: Protocol value is not 1 or 3!! Protocol value: {protocolValue}");
}
Logger.Stub?.PrintStub(LogClass.ServiceLdn, $"Protocol value: {protocolValue}");
return ResultCode.Success;
}
[CommandCmif(200)] [CommandCmif(200)]
// OpenAccessPoint() // OpenAccessPoint()
public ResultCode OpenAccessPoint(ServiceCtx context) public ResultCode OpenAccessPoint(ServiceCtx context)

View File

@@ -21,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; } public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; }
public nint Handle => nint.Zero; public nint Handle => IntPtr.Zero;
public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint; public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;

View File

@@ -33,20 +33,7 @@ namespace Ryujinx.Horizon.Prepo.Ipc
[CmifCommand(10100)] // 1.0.0-5.1.0 [CmifCommand(10100)] // 1.0.0-5.1.0
[CmifCommand(10102)] // 6.0.0-9.2.0 [CmifCommand(10102)] // 6.0.0-9.2.0
[CmifCommand(10104)] // 10.0.0+ [CmifCommand(10104)] // 10.0.0+
public Result SaveReportOld([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid) public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
{
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
{
return PrepoResult.PermissionDenied;
}
ProcessPlayReport(PlayReportKind.Normal, gameRoomBuffer, reportBuffer, pid, Uid.Null);
return Result.Success;
}
[CmifCommand(10106)] // 21.0.0+
public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid, bool optInCheckEnabled)
{ {
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0) if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
{ {
@@ -61,20 +48,7 @@ namespace Ryujinx.Horizon.Prepo.Ipc
[CmifCommand(10101)] // 1.0.0-5.1.0 [CmifCommand(10101)] // 1.0.0-5.1.0
[CmifCommand(10103)] // 6.0.0-9.2.0 [CmifCommand(10103)] // 6.0.0-9.2.0
[CmifCommand(10105)] // 10.0.0+ [CmifCommand(10105)] // 10.0.0+
public Result SaveReportWithUserOld(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid) public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
{
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
{
return PrepoResult.PermissionDenied;
}
ProcessPlayReport(PlayReportKind.Normal, gameRoomBuffer, reportBuffer, pid, userId, true);
return Result.Success;
}
[CmifCommand(10107)] // 21.0.0+
public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid, bool optInCheckEnabled)
{ {
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0) if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
{ {

View File

@@ -8,10 +8,8 @@ namespace Ryujinx.Horizon.Sdk.Prepo
{ {
interface IPrepoService : IServiceObject interface IPrepoService : IServiceObject
{ {
Result SaveReportOld(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid); Result SaveReport(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
Result SaveReport(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid, bool optInCheckEnabled); Result SaveReportWithUser(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
Result SaveReportWithUserOld(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
Result SaveReportWithUser(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid, bool optInCheckEnabled);
Result RequestImmediateTransmission(); Result RequestImmediateTransmission();
Result GetTransmissionStatus(out int status); Result GetTransmissionStatus(out int status);
Result GetSystemSessionId(out ulong systemSessionId); Result GetSystemSessionId(out ulong systemSessionId);

View File

@@ -9,20 +9,10 @@ using static SDL.SDL3;
namespace Ryujinx.Input.SDL3 namespace Ryujinx.Input.SDL3
{ {
public unsafe class SDL3GamepadDriver : IGamepadDriver public unsafe class SDL3GamepadDriver : IGamepadDriver
{ {
private readonly Dictionary<SDL_JoystickID, string> _gamepadsInstanceIdsMapping; private readonly Dictionary<SDL_JoystickID, string> _gamepadsInstanceIdsMapping;
private readonly Dictionary<SDL_JoystickID, string> _gamepadsIds; private readonly Dictionary<SDL_JoystickID, string> _gamepadsIds;
/// <summary>
/// Unlinked joy-cons
/// </summary>
private readonly Dictionary<SDL_JoystickID, string> _joyConsIds;
/// <summary>
/// Linked joy-cons, remove dual joy-con from <c>_gamepadsIds</c> when a linked joy-con is removed
/// </summary>
private readonly Dictionary<SDL_JoystickID,string> _linkedJoyConsIds;
private readonly Lock _lock = new(); private readonly Lock _lock = new();
public ReadOnlySpan<string> GamepadsIds public ReadOnlySpan<string> GamepadsIds
@@ -31,11 +21,7 @@ namespace Ryujinx.Input.SDL3
{ {
lock (_lock) lock (_lock)
{ {
List<string> temp = []; return _gamepadsIds.Values.ToArray();
temp.AddRange(_gamepadsIds.Values);
temp.AddRange(_joyConsIds.Values);
temp.AddRange(_linkedJoyConsIds.Values);
return temp.ToArray();
} }
} }
} }
@@ -49,8 +35,6 @@ namespace Ryujinx.Input.SDL3
{ {
_gamepadsInstanceIdsMapping = new Dictionary<SDL_JoystickID, string>(); _gamepadsInstanceIdsMapping = new Dictionary<SDL_JoystickID, string>();
_gamepadsIds = []; _gamepadsIds = [];
_joyConsIds = [];
_linkedJoyConsIds = [];
SDL3Driver.Instance.Initialize(); SDL3Driver.Instance.Initialize();
SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected; SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
@@ -108,7 +92,7 @@ namespace Ryujinx.Input.SDL3
int guidIndex = 0; int guidIndex = 0;
id = guidIndex + "-" + guidString; id = guidIndex + "-" + guidString;
while (_gamepadsIds.ContainsValue(id) || _joyConsIds.ContainsValue(id) || _linkedJoyConsIds.ContainsValue(id)) while (_gamepadsIds.ContainsValue(id))
{ {
id = (++guidIndex) + "-" + guidString; id = (++guidIndex) + "-" + guidString;
} }
@@ -120,47 +104,16 @@ namespace Ryujinx.Input.SDL3
private void HandleJoyStickDisconnected(SDL_JoystickID joystickInstanceId) private void HandleJoyStickDisconnected(SDL_JoystickID joystickInstanceId)
{ {
bool joyConPairDisconnected = false; bool joyConPairDisconnected = false;
string fakeId = null;
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id)) if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
return; return;
lock (_lock) lock (_lock)
{ {
if (!_linkedJoyConsIds.ContainsKey(joystickInstanceId)) _gamepadsIds.Remove(joystickInstanceId);
if (!SDL3JoyConPair.IsCombinable(_gamepadsIds))
{ {
if (!_joyConsIds.Remove(joystickInstanceId)) _gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id));
{
_gamepadsIds.Remove(joystickInstanceId);
}
}
else
{
foreach (string matchId in _gamepadsIds.Values)
{
if (matchId.Contains(id))
{
fakeId = matchId;
break;
}
}
string leftId = fakeId!.Split('_')[0];
string rightId = fakeId!.Split('_')[1];
if (leftId == id)
{
_linkedJoyConsIds.Remove(GetInstanceIdFromId(rightId));
_joyConsIds.Add(GetInstanceIdFromId(rightId), rightId);
}
else
{
_linkedJoyConsIds.Remove(GetInstanceIdFromId(leftId));
_joyConsIds.Add(GetInstanceIdFromId(leftId), leftId);
}
_linkedJoyConsIds.Remove(joystickInstanceId);
_gamepadsIds.Remove(GetInstanceIdFromId(fakeId));
joyConPairDisconnected = true; joyConPairDisconnected = true;
} }
} }
@@ -168,14 +121,13 @@ namespace Ryujinx.Input.SDL3
OnGamepadDisconnected?.Invoke(id); OnGamepadDisconnected?.Invoke(id);
if (joyConPairDisconnected) if (joyConPairDisconnected)
{ {
OnGamepadDisconnected?.Invoke(fakeId); OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id);
} }
} }
private void HandleJoyStickConnected(SDL_JoystickID joystickInstanceId) private void HandleJoyStickConnected(SDL_JoystickID joystickInstanceId)
{ {
bool joyConPairConnected = false; bool joyConPairConnected = false;
string fakeId = null;
if (SDL_IsGamepad(joystickInstanceId)) if (SDL_IsGamepad(joystickInstanceId))
{ {
@@ -197,40 +149,27 @@ namespace Ryujinx.Input.SDL3
{ {
lock (_lock) lock (_lock)
{ {
if (!SDL3JoyCon.IsJoyCon(joystickInstanceId))
_gamepadsIds.Add(joystickInstanceId, id);
if (SDL3JoyConPair.IsCombinable(_gamepadsIds))
{ {
_gamepadsIds.Add(joystickInstanceId, id); // TODO - It appears that you can only have one joy con pair connected at a time?
} // This was also the behavior before SDL3
else _gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id));
{ uint fakeInstanceID = uint.MaxValue;
if (SDL3JoyConPair.IsCombinable(joystickInstanceId, _joyConsIds, out SDL_JoystickID match)) while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceID, SDL3JoyConPair.Id))
{ {
_joyConsIds.Remove(match, out string matchId); fakeInstanceID--;
_linkedJoyConsIds.Add(joystickInstanceId, id);
_linkedJoyConsIds.Add(match, matchId);
uint fakeInstanceId = uint.MaxValue;
fakeId = SDL3JoyCon.IsLeftJoyCon(joystickInstanceId)
? $"{id}_{matchId}"
: $"{matchId}_{id}";
while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceId, fakeId))
{
fakeInstanceId--;
}
_gamepadsInstanceIdsMapping.Add((SDL_JoystickID)fakeInstanceId, fakeId);
joyConPairConnected = true;
}
else
{
_joyConsIds.Add(joystickInstanceId, id);
} }
joyConPairConnected = true;
} }
} }
OnGamepadConnected?.Invoke(id); OnGamepadConnected?.Invoke(id);
if (joyConPairConnected) if (joyConPairConnected)
{ {
OnGamepadConnected?.Invoke(fakeId); OnGamepadConnected?.Invoke(SDL3JoyConPair.Id);
} }
} }
} }
@@ -254,22 +193,10 @@ namespace Ryujinx.Input.SDL3
{ {
OnGamepadDisconnected?.Invoke(gamepad.Value); OnGamepadDisconnected?.Invoke(gamepad.Value);
} }
foreach (var gamepad in _joyConsIds)
{
OnGamepadDisconnected?.Invoke(gamepad.Value);
}
foreach (var gamepad in _linkedJoyConsIds)
{
OnGamepadDisconnected?.Invoke(gamepad.Value);
}
lock (_lock) lock (_lock)
{ {
_gamepadsIds.Clear(); _gamepadsIds.Clear();
_joyConsIds.Clear();
_linkedJoyConsIds.Clear();
} }
SDL3Driver.Instance.Dispose(); SDL3Driver.Instance.Dispose();
@@ -288,27 +215,11 @@ namespace Ryujinx.Input.SDL3
public IGamepad GetGamepad(string id) public IGamepad GetGamepad(string id)
{ {
// joy-con pair ids is the combined ids of its parts which are split using a '_' if (id == SDL3JoyConPair.Id)
if (id.Contains('_'))
{ {
lock (_lock) lock (_lock)
{ {
string leftId = id.Split('_')[0]; return SDL3JoyConPair.GetGamepad(_gamepadsIds);
string rightId = id.Split('_')[1];
SDL_JoystickID leftInstanceId = GetInstanceIdFromId(leftId);
SDL_JoystickID rightInstanceId = GetInstanceIdFromId(rightId);
SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad(leftInstanceId);
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad(rightInstanceId);
if (leftGamepadHandle == null || rightGamepadHandle == null)
{
return null;
}
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, leftId),
new SDL3JoyCon(rightGamepadHandle, rightId));
} }
} }
@@ -321,7 +232,7 @@ namespace Ryujinx.Input.SDL3
return null; return null;
} }
if (SDL3JoyCon.IsJoyCon(instanceId)) if (SDL_GetGamepadName(gamepadHandle).StartsWith(SDL3JoyCon.Prefix))
{ {
return new SDL3JoyCon(gamepadHandle, id); return new SDL3JoyCon(gamepadHandle, id);
} }
@@ -338,22 +249,6 @@ namespace Ryujinx.Input.SDL3
yield return GetGamepad(gamepad.Value); yield return GetGamepad(gamepad.Value);
} }
} }
lock (_joyConsIds)
{
foreach (var gamepad in _joyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
lock (_linkedJoyConsIds)
{
foreach (var gamepad in _linkedJoyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
} }
} }
} }

View File

@@ -24,10 +24,10 @@ namespace Ryujinx.Input.SDL3
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _leftButtonsDriverMapping = new() private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _leftButtonsDriverMapping = new()
{ {
{GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK}, {GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK},
{GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST}, {GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
{GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST}, {GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
{GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH}, {GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
{GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH}, {GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
{GamepadButtonInputId.Minus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START}, {GamepadButtonInputId.Minus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START},
{GamepadButtonInputId.LeftShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1}, {GamepadButtonInputId.LeftShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
{GamepadButtonInputId.LeftTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2}, {GamepadButtonInputId.LeftTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
@@ -37,10 +37,10 @@ namespace Ryujinx.Input.SDL3
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _rightButtonsDriverMapping = new() private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _rightButtonsDriverMapping = new()
{ {
{GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK}, {GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK},
{GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH}, {GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
{GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST}, {GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
{GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST}, {GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
{GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH}, {GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
{GamepadButtonInputId.Plus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START}, {GamepadButtonInputId.Plus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START},
{GamepadButtonInputId.RightShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1}, {GamepadButtonInputId.RightShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
{GamepadButtonInputId.RightTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2}, {GamepadButtonInputId.RightTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
@@ -398,15 +398,5 @@ namespace Ryujinx.Input.SDL3
return SDL_GetGamepadButton(_gamepadHandle, button); return SDL_GetGamepadButton(_gamepadHandle, button);
} }
public static bool IsJoyCon(SDL_JoystickID gamepadsId)
{
return SDL_GetGamepadNameForID(gamepadsId) is LeftName or RightName;
}
public static bool IsLeftJoyCon(SDL_JoystickID gamepadsId)
{
return SDL_GetGamepadNameForID(gamepadsId) is LeftName;
}
} }
} }

View File

@@ -15,7 +15,7 @@ namespace Ryujinx.Input.SDL3
public const string Id = "JoyConPair"; public const string Id = "JoyConPair";
string IGamepad.Id => Id; string IGamepad.Id => Id;
public string Name => "Nintendo Switch Dual Joy-Con (L/R)"; public string Name => "* Nintendo Switch Joy-Con (L/R)";
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true }; public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
public void Dispose() public void Dispose()
@@ -96,23 +96,44 @@ namespace Ryujinx.Input.SDL3
right.SetTriggerThreshold(triggerThreshold); right.SetTriggerThreshold(triggerThreshold);
} }
public static bool IsCombinable(SDL_JoystickID joyCon1, Dictionary<SDL_JoystickID, string> joyConIds, out SDL_JoystickID match) public static bool IsCombinable(Dictionary<SDL_JoystickID, string> gamepadsIds)
{ {
bool isLeft = SDL3JoyCon.IsLeftJoyCon(joyCon1); (int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
string matchName = isLeft ? SDL3JoyCon.RightName : SDL3JoyCon.LeftName; return leftIndex >= 0 && rightIndex >= 0;
match = 0; }
foreach (var joyConId in joyConIds.Keys) private static (int leftIndex, int rightIndex) DetectJoyConPair(Dictionary<SDL_JoystickID, string> gamepadsIds)
{
Dictionary<string, SDL_JoystickID> gamepadNames = gamepadsIds
.Where(gamepadId => gamepadId.Value != Id && SDL_GetGamepadNameForID(gamepadId.Key) is SDL3JoyCon.LeftName or SDL3JoyCon.RightName)
.Select(gamepad => (SDL_GetGamepadNameForID(gamepad.Key), gamepad.Key))
.ToDictionary();
SDL_JoystickID idx;
int leftIndex = gamepadNames.TryGetValue(SDL3JoyCon.LeftName, out idx) ? (int)idx : -1;
int rightIndex = gamepadNames.TryGetValue(SDL3JoyCon.RightName, out idx) ? (int)idx : -1;
return (leftIndex, rightIndex);
}
public unsafe static IGamepad GetGamepad(Dictionary<SDL_JoystickID, string> gamepadsIds)
{
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
if (leftIndex <= 0 || rightIndex <= 0)
{ {
if (SDL_GetGamepadNameForID(joyConId) == matchName) return null;
{
match = joyConId;
return true;
}
} }
return false; SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)leftIndex);
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)rightIndex);
if (leftGamepadHandle == null || rightGamepadHandle == null)
{
return null;
}
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, gamepadsIds[(SDL_JoystickID)leftIndex]),
new SDL3JoyCon(rightGamepadHandle, gamepadsIds[(SDL_JoystickID)rightIndex]));
} }
} }
} }

View File

@@ -159,7 +159,7 @@ namespace Ryujinx.Memory.WindowsShared
{ {
SplitForMap((ulong)location, (ulong)size, srcOffset); SplitForMap((ulong)location, (ulong)size, srcOffset);
nint ptr = WindowsApi.MapViewOfFile3( IntPtr ptr = WindowsApi.MapViewOfFile3(
sharedMemory, sharedMemory,
WindowsApi.CurrentProcessHandle, WindowsApi.CurrentProcessHandle,
location, location,

View File

@@ -227,7 +227,7 @@ namespace Ryujinx.Tests.Memory
// Create some info to be used for managing the native writing loop. // Create some info to be used for managing the native writing loop.
int stateSize = Unsafe.SizeOf<NativeWriteLoopState>(); int stateSize = Unsafe.SizeOf<NativeWriteLoopState>();
nint statePtr = Marshal.AllocHGlobal(stateSize); IntPtr statePtr = Marshal.AllocHGlobal(stateSize);
Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize); Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize);
ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef<NativeWriteLoopState>((void*)statePtr); ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef<NativeWriteLoopState>((void*)statePtr);

View File

@@ -1,5 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.Threading; using Avalonia.Threading;
using CommandLine;
using DiscordRPC; using DiscordRPC;
using Gommon; using Gommon;
using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia;
@@ -78,28 +79,38 @@ namespace Ryujinx.Ava
} }
} }
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui"); PreviewerDetached = true;
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
if (ConsumeCommandLineArgument(ref args, "--no-gui")
|| ConsumeCommandLineArgument(ref args, "nogui"))
{
try
{
HeadlessRyujinx.Entrypoint(args);
return 0;
}
catch (Exception e)
{
Logger.Error?.PrintMsg(LogClass.Application, $"Exception occurred when running Headless Ryujinx: {e.Message}\n{e.StackTrace}");
return 1;
}
}
if (!Initialize(args, out RyujinxOptions options))
{
Logger.Flush();
return 1;
}
// TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception. // TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception.
// This is undesirable and causes very odd behavior during development (the process stops responding, // This is undesirable and causes very odd behavior during development (the process stops responding,
// the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user. // the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user.
// This needs to be investigated, but calling prctl() is better than modifying system-wide settings or leaving this be. // This needs to be investigated, but calling prctl() is better than modifying system-wide settings or leaving this be.
if (!coreDumpArg) if (!options.CoreDumpsEnabled)
{ {
OsUtils.SetCoreDumpable(false); OsUtils.SetCoreDumpable(false);
} }
PreviewerDetached = true;
if (noGuiArg)
{
HeadlessRyujinx.Entrypoint(args);
return 0;
}
Initialize(args);
LoggerAdapter.Register(); LoggerAdapter.Register();
IconProvider.Current IconProvider.Current
@@ -137,13 +148,14 @@ namespace Ryujinx.Ava
return found; return found;
} }
private static void Initialize(string[] args) private static Result Initialize(string[] args, out RyujinxOptions options)
{ {
// Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched // Ensure Discord presence timestamp begins at the absolute start of when Ryujinx is launched
DiscordIntegrationModule.EmulatorStartedAt = Timestamps.Now; DiscordIntegrationModule.EmulatorStartedAt = Timestamps.Now;
// Parse arguments // Parse arguments
CommandLineState.ParseArguments(args); Result res = RyujinxOptions.Read(args, out options);
if (!res) return res;
if (OperatingSystem.IsMacOS()) if (OperatingSystem.IsMacOS())
{ {
@@ -163,7 +175,7 @@ namespace Ryujinx.Ava
AppDomain.CurrentDomain.ProcessExit += (_, _) => Exit(); AppDomain.CurrentDomain.ProcessExit += (_, _) => Exit();
// Setup base data directory. // Setup base data directory.
AppDataManager.Initialize(CommandLineState.BaseDirPathArg); AppDataManager.Initialize(options.EmuDataBaseDirPath);
// Initialize the configuration. // Initialize the configuration.
ConfigurationState.Initialize(); ConfigurationState.Initialize();
@@ -196,10 +208,12 @@ namespace Ryujinx.Ava
} }
} }
if (CommandLineState.LaunchPathArg != null) if (options.LaunchPath != null)
{ {
MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.LaunchApplicationId, CommandLineState.StartFullscreenArg); MainWindow.DeferLoadApplication(options.LaunchPath, options.LaunchApplicationId, options.StartFullscreen);
} }
return Result.Success;
} }
public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false) public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false)
@@ -222,7 +236,6 @@ namespace Ryujinx.Ava
public static void ReloadConfig(bool isRunGameWithCustomConfig = false) public static void ReloadConfig(bool isRunGameWithCustomConfig = false)
{ {
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName); string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName); string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
@@ -273,74 +286,50 @@ namespace Ryujinx.Ava
UseHardwareAcceleration = ConfigurationState.Instance.EnableHardwareAcceleration; UseHardwareAcceleration = ConfigurationState.Instance.EnableHardwareAcceleration;
// Check if graphics backend was overridden // Check if graphics backend was overridden
if (CommandLineState.OverrideGraphicsBackend is not null) if (RyujinxOptions.Shared.GraphicsBackendOverride is not null)
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = CommandLineState.OverrideGraphicsBackend.ToLower() switch ConfigurationState.Instance.Graphics.GraphicsBackend.Value =
{ RyujinxOptions.Shared.GraphicsBackendOverride.Value;
"opengl" => GraphicsBackend.OpenGl,
"vulkan" => GraphicsBackend.Vulkan,
_ => ConfigurationState.Instance.Graphics.GraphicsBackend
};
// Check if backend threading was overridden // Check if backend threading was overridden
if (CommandLineState.OverrideBackendThreading is not null) if (RyujinxOptions.Shared.BackendThreadingOverride is not null)
ConfigurationState.Instance.Graphics.BackendThreading.Value = CommandLineState.OverrideBackendThreading.ToLower() switch ConfigurationState.Instance.Graphics.BackendThreading.Value =
{ RyujinxOptions.Shared.BackendThreadingOverride.Value;
"auto" => BackendThreading.Auto,
"off" => BackendThreading.Off, if (RyujinxOptions.Shared.BackendThreadingOverrideAfterReboot is not null)
"on" => BackendThreading.On, BackendThreadingArg = RyujinxOptions.Shared.BackendThreadingOverrideAfterReboot.Value.ToString();
_ => ConfigurationState.Instance.Graphics.BackendThreading
};
if (CommandLineState.OverrideBackendThreadingAfterReboot is not null)
{
BackendThreadingArg = CommandLineState.OverrideBackendThreadingAfterReboot;
}
// Check if docked mode was overriden. // Check if docked mode was overriden.
if (CommandLineState.OverrideDockedMode.HasValue) if (RyujinxOptions.Shared.DockedModeOverride.HasValue)
ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value; ConfigurationState.Instance.System.EnableDockedMode.Value =
RyujinxOptions.Shared.DockedModeOverride.Value;
// Check if HideCursor was overridden. // Check if HideCursor was overridden.
if (CommandLineState.OverrideHideCursor is not null) if (RyujinxOptions.Shared.HideCursorOverride is not null)
ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor.ToLower() switch ConfigurationState.Instance.HideCursor.Value = RyujinxOptions.Shared.HideCursorOverride.Value;
{
"never" => HideCursorMode.Never,
"onidle" => HideCursorMode.OnIdle,
"always" => HideCursorMode.Always,
_ => ConfigurationState.Instance.HideCursor,
};
// Check if memoryManagerMode was overridden. // Check if memoryManagerMode was overridden.
if (CommandLineState.OverrideMemoryManagerMode is not null) if (RyujinxOptions.Shared.MemoryManagerModeOverride is not null)
if (Enum.TryParse(CommandLineState.OverrideMemoryManagerMode, true, out MemoryManagerMode result)) ConfigurationState.Instance.System.MemoryManagerMode.Value = RyujinxOptions.Shared.MemoryManagerModeOverride.Value;
{
ConfigurationState.Instance.System.MemoryManagerMode.Value = result;
}
// Check if PPTC was overridden. // Check if PPTC was overridden.
if (CommandLineState.OverridePPTC is not null) if (RyujinxOptions.Shared.PptcOverride is not null)
if (Enum.TryParse(CommandLineState.OverridePPTC, true, out bool result)) if (Enum.TryParse(RyujinxOptions.Shared.PptcOverride, true, out bool result))
{ {
ConfigurationState.Instance.System.EnablePtc.Value = result; ConfigurationState.Instance.System.EnablePtc.Value = result;
} }
// Check if region was overridden. // Check if region was overridden.
if (CommandLineState.OverrideSystemRegion is not null) if (RyujinxOptions.Shared.SystemRegionOverride is not null)
if (Enum.TryParse(CommandLineState.OverrideSystemRegion, true, out Region result)) ConfigurationState.Instance.System.Region.Value = RyujinxOptions.Shared.SystemRegionOverride.Value;
{
ConfigurationState.Instance.System.Region.Value = result;
}
//Check if language was overridden. //Check if language was overridden.
if (CommandLineState.OverrideSystemLanguage is not null) if (RyujinxOptions.Shared.SystemLanguageOverride is not null)
if (Enum.TryParse(CommandLineState.OverrideSystemLanguage, true, out Language result)) ConfigurationState.Instance.System.Language.Value = RyujinxOptions.Shared.SystemLanguageOverride.Value;
{
ConfigurationState.Instance.System.Language.Value = result;
}
// Check if hardware-acceleration was overridden. // Check if hardware-acceleration was overridden.
if (CommandLineState.OverrideHardwareAcceleration != null) if (RyujinxOptions.Shared.HardwareAccelerationOverride is not null)
UseHardwareAcceleration = CommandLineState.OverrideHardwareAcceleration.Value; UseHardwareAcceleration = RyujinxOptions.Shared.HardwareAccelerationOverride.Value;
} }
internal static void PrintSystemInfo() internal static void PrintSystemInfo()

View File

@@ -77,13 +77,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" /> <ProjectReference Include="..\Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" /> <ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj" /> <ProjectReference Include="..\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.Apple\Ryujinx.Audio.Backends.Apple.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" /> <ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" /> <ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" /> <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />

View File

@@ -6,7 +6,6 @@ using Avalonia.Threading;
using DiscordRPC; using DiscordRPC;
using LibHac.Common; using LibHac.Common;
using LibHac.Ns; using LibHac.Ns;
using Ryujinx.Audio.Backends.Apple;
using Ryujinx.Audio.Backends.Dummy; using Ryujinx.Audio.Backends.Dummy;
using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL3; using Ryujinx.Audio.Backends.SDL3;
@@ -950,9 +949,6 @@ namespace Ryujinx.Ava.Systems
AudioBackend.Dummy AudioBackend.Dummy
]; ];
if (OperatingSystem.IsMacOS())
availableBackends.Insert(0, AudioBackend.AudioToolbox);
AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value; AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value;
if (preferredBackend is AudioBackend.SDL2) if (preferredBackend is AudioBackend.SDL2)
@@ -989,9 +985,6 @@ namespace Ryujinx.Ava.Systems
deviceDriver = currentBackend switch deviceDriver = currentBackend switch
{ {
#pragma warning disable CA1416 // Platform compatibility is enforced in AppleHardwareDeviceDriver.IsSupported, before any potentially platform-sensitive code can run.
AudioBackend.AudioToolbox => InitializeAudioBackend<AppleHardwareDeviceDriver>(AudioBackend.AudioToolbox, nextBackend),
#pragma warning restore CA1416
AudioBackend.SDL3 => InitializeAudioBackend<SDL3HardwareDeviceDriver>(AudioBackend.SDL3, nextBackend), AudioBackend.SDL3 => InitializeAudioBackend<SDL3HardwareDeviceDriver>(AudioBackend.SDL3, nextBackend),
AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend), AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend),
AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend), AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend),

View File

@@ -9,7 +9,6 @@ namespace Ryujinx.Ava.Systems.Configuration
OpenAl, OpenAl,
SoundIo, SoundIo,
SDL3, SDL3,
AudioToolbox,
SDL2 = SDL3 SDL2 = SDL3
} }
} }

View File

@@ -7,7 +7,6 @@ using Ryujinx.Common.Logging;
using Ryujinx.Systems.Update.Client; using Ryujinx.Systems.Update.Client;
using Ryujinx.Systems.Update.Common; using Ryujinx.Systems.Update.Common;
using System; using System;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -47,12 +46,6 @@ namespace Ryujinx.Ava.Systems
return Return<VersionResponse>.Failure( return Return<VersionResponse>.Failure(
new MessageError("DNS resolution error occurred. Is your internet down?")); new MessageError("DNS resolution error occurred. Is your internet down?"));
} }
catch (HttpRequestException hre)
when (hre.StatusCode is HttpStatusCode.BadGateway)
{
return Return<VersionResponse>.Failure(
new MessageError("Could not connect to the update server, but it appears like you have internet. It seems like the update server is offline, try again later."));
}
} }
public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false) public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)

View File

@@ -181,7 +181,7 @@ namespace Ryujinx.Ava.Systems
if (shouldRestart) if (shouldRestart)
{ {
List<string> arguments = CommandLineState.Arguments.ToList(); List<string> arguments = RyujinxOptions.Shared.InputArguments.ToList();
string executableDirectory = AppDomain.CurrentDomain.BaseDirectory; string executableDirectory = AppDomain.CurrentDomain.BaseDirectory;
// On macOS we perform the update at relaunch. // On macOS we perform the update at relaunch.
@@ -218,7 +218,7 @@ namespace Ryujinx.Ava.Systems
WorkingDirectory = executableDirectory, WorkingDirectory = executableDirectory,
}; };
foreach (string argument in CommandLineState.Arguments) foreach (string argument in arguments)
{ {
processStart.ArgumentList.Add(argument); processStart.ArgumentList.Add(argument);
} }

View File

@@ -16,31 +16,41 @@
<Design.DataContext> <Design.DataContext>
<viewModels:ProfileSelectorDialogViewModel /> <viewModels:ProfileSelectorDialogViewModel />
</Design.DataContext> </Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto">
<Border Padding="-3" BorderThickness="0">
<Border
CornerRadius="5"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1">
<ListBox <ListBox
HorizontalAlignment="Center" MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="Transparent" Background="Transparent"
ItemsSource="{Binding Profiles}" ItemsSource="{Binding Profiles}"
SelectionChanged="ProfilesList_SelectionChanged"> SelectionChanged="ProfilesList_SelectionChanged">
<ListBox.ItemsPanel> <ListBox.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<WrapPanel <WrapPanel
HorizontalAlignment="Center" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
Orientation="Horizontal"/> Orientation="Horizontal" />
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ListBox.ItemsPanel> </ListBox.ItemsPanel>
<ListBox.Styles> <ListBox.Styles>
<Style Selector="ListBoxItem"> <Style Selector="ListBoxItem">
<Setter Property="Margin" Value="0" /> <Setter Property="Margin" Value="5 5 0 5" />
<Setter Property="CornerRadius" Value="5" /> <Setter Property="CornerRadius" Value="5" />
</Style> </Style>
<Style Selector="Rectangle#SelectionIndicator"> <Style Selector="Rectangle#SelectionIndicator">
<Setter Property="Opacity" Value="0" /> <Setter Property="Opacity" Value="0" />
</Style> </Style>
</ListBox.Styles> </ListBox.Styles>
<ListBox.DataTemplates> <ListBox.DataTemplates>
<DataTemplate <DataTemplate
DataType="models:UserProfile"> DataType="models:UserProfile">
@@ -48,7 +58,6 @@
PointerEntered="Grid_PointerEntered" PointerEntered="Grid_PointerEntered"
PointerExited="Grid_OnPointerExited"> PointerExited="Grid_OnPointerExited">
<Border <Border
Margin="5"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ClipToBounds="True" ClipToBounds="True"
@@ -60,26 +69,37 @@
<Image <Image
Width="96" Width="96"
Height="96" Height="96"
Margin="0,0,0,10"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Top" VerticalAlignment="Top"
Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" /> Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
<TextBlock <TextBlock
Text="{Binding Name}" HorizontalAlignment="Stretch"
Height="30"
MaxWidth="90" MaxWidth="90"
Text="{Binding Name}"
TextAlignment="Center" TextAlignment="Center"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap" TextWrapping="Wrap"
MaxLines="2" /> TextTrimming="CharacterEllipsis"
MaxLines="2"
Margin="5" />
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
<DataTemplate
DataType="viewModels:BaseModel">
<Panel
Height="118"
Width="96">
<Panel.Styles>
<Style Selector="Panel">
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
</Style>
</Panel.Styles>
</Panel>
</DataTemplate>
</ListBox.DataTemplates> </ListBox.DataTemplates>
</ListBox> </ListBox>
</Border> </Border>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -94,7 +94,7 @@ namespace Ryujinx.Ava.UI.Applet
ContentDialog contentDialog = new() ContentDialog contentDialog = new()
{ {
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle], Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue], PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
SecondaryButtonText = string.Empty, SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel], CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],

View File

@@ -9,11 +9,11 @@
Command="{Binding RunApplication}" Command="{Binding RunApplication}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Header="{ext:Locale GameListContextMenuRunApplication}" Header="{ext:Locale GameListContextMenuRunApplication}"
Icon="{ext:Icon fa-solid fa-play}" /> Icon="{ext:Icon fa-solid fa-play}"/>
<MenuItem <MenuItem
Command="{Binding ToggleFavorite}" Command="{Binding ToggleFavorite}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Header="{Binding FavoriteStatusText}" Header="{ext:Locale GameListContextMenuToggleFavorite}"
Icon="{ext:Icon fa-solid fa-star}" /> Icon="{ext:Icon fa-solid fa-star}" />
<MenuItem <MenuItem
Command="{Binding CreateApplicationShortcut}" Command="{Binding CreateApplicationShortcut}"
@@ -30,15 +30,15 @@
ToolTip.Tip="{ext:Locale EditCustomConfigurationToolTip}" /> ToolTip.Tip="{ext:Locale EditCustomConfigurationToolTip}" />
<MenuItem <MenuItem
Command="{Binding EditGameConfiguration}" Command="{Binding EditGameConfiguration}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
IsVisible="{Binding !SelectedApplication.HasIndependentConfiguration}" IsVisible="{Binding !SelectedApplication.HasIndependentConfiguration}"
Header="{ext:Locale GameListContextMenuCreateCustomConfiguration}" Header="{ext:Locale GameListContextMenuCreateCustomConfiguration}"
Icon="{ext:Icon fa-solid fa-gear}" Icon="{ext:Icon fa-solid fa-gear}"
ToolTip.Tip="{ext:Locale CreateCustomConfigurationToolTip}" /> ToolTip.Tip="{ext:Locale CreateCustomConfigurationToolTip}" />
<MenuItem <MenuItem
IsVisible="{Binding HasCompatibilityEntry}"
Command="{Binding OpenApplicationCompatibility}" Command="{Binding OpenApplicationCompatibility}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
IsVisible="{Binding HasCompatibilityEntry}"
Header="{ext:Locale GameListContextMenuShowCompatEntry}" Header="{ext:Locale GameListContextMenuShowCompatEntry}"
Icon="{ext:Icon fa-solid fa-database}" Icon="{ext:Icon fa-solid fa-database}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/> ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
@@ -104,8 +104,8 @@
Command="{Binding TrimXci}" Command="{Binding TrimXci}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
Header="{ext:Locale GameListContextMenuTrimXCI}" Header="{ext:Locale GameListContextMenuTrimXCI}"
Icon="{ext:Icon fa-solid fa-scissors}"
IsEnabled="{Binding TrimXCIEnabled}" IsEnabled="{Binding TrimXCIEnabled}"
Icon="{ext:Icon fa-solid fa-scissors}"
ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" /> ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" />
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon fa-solid fa-memory}"> <MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon fa-solid fa-memory}">
<MenuItem <MenuItem
@@ -151,9 +151,9 @@
Header="{ext:Locale GameListContextMenuExtractDataRomFS}" Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" /> ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem <MenuItem
IsVisible="{Binding HasDlc}"
Command="{Binding ExtractApplicationAocRomFs}" Command="{Binding ExtractApplicationAocRomFs}"
CommandParameter="{Binding}" CommandParameter="{Binding}"
IsVisible="{Binding HasDlc}"
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}" Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" /> ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />
<MenuItem <MenuItem

View File

@@ -71,7 +71,7 @@ namespace Ryujinx.Ava.UI.Controls
NavigationDialogHost content = new(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient); NavigationDialogHost content = new(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
ContentDialog contentDialog = new() ContentDialog contentDialog = new()
{ {
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle], Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
PrimaryButtonText = string.Empty, PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty, SecondaryButtonText = string.Empty,
CloseButtonText = string.Empty, CloseButtonText = string.Empty,
@@ -156,7 +156,7 @@ namespace Ryujinx.Ava.UI.Controls
{ {
_ = Dispatcher.UIThread.InvokeAsync(async () _ = Dispatcher.UIThread.InvokeAsync(async ()
=> await ContentDialogHelper.CreateErrorDialog( => await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileDeletionWarningMessage])); LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]));
return; return;
} }
@@ -165,8 +165,8 @@ namespace Ryujinx.Ava.UI.Controls
} }
UserResult result = await ContentDialogHelper.CreateConfirmationDialog( UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileDeletionConfirmMessage], LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage],
LocaleManager.Instance[LocaleKeys.IrreversibleActionNote], string.Empty,
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.InputDialogNo],
string.Empty); string.Empty);

View File

@@ -10,9 +10,6 @@ namespace Ryujinx.Ava.UI.Models
[ObservableProperty] [ObservableProperty]
public partial byte[] Image { get; set; } public partial byte[] Image { get; set; }
[ObservableProperty]
public partial bool FirmwareFound { get; set; }
[ObservableProperty] [ObservableProperty]
public partial string Name { get; set; } = string.Empty; public partial string Name { get; set; } = string.Empty;

View File

@@ -95,7 +95,7 @@ namespace Ryujinx.Ava
if (result == UserResult.Yes) if (result == UserResult.Yes)
{ {
_ = Process.Start(Environment.ProcessPath!, CommandLineState.Arguments); _ = Process.Start(Environment.ProcessPath!, RyujinxOptions.Shared.InputArguments);
desktop.Shutdown(); desktop.Shutdown();
Environment.Exit(0); Environment.Exit(0);
} }

View File

@@ -255,41 +255,27 @@ namespace Ryujinx.Ava.UI.ViewModels
return amiiboJson; return amiiboJson;
} }
private async Task<AmiiboJson?> ReadLocalJsonFileAsync() private AmiiboJson? ReadLocalJsonFile()
{ {
bool isValid = false; bool isValid = false;
AmiiboJson amiiboJson = new(); AmiiboJson amiiboJson = new();
try try
{ {
try if (File.Exists(_amiiboJsonPath))
{ {
if (File.Exists(_amiiboJsonPath)) isValid = TryGetAmiiboJson(File.ReadAllText(_amiiboJsonPath), out amiiboJson);
{
isValid = TryGetAmiiboJson(await File.ReadAllTextAsync(_amiiboJsonPath), out amiiboJson);
}
}
catch (Exception exception)
{
Logger.Warning?.Print(LogClass.Application, $"Unable to read data from '{_amiiboJsonPath}': {exception}");
isValid = false;
}
if (!isValid)
{
return null;
} }
} }
catch (Exception exception) catch (Exception exception)
{ {
if (!isValid) Logger.Warning?.Print(LogClass.Application, $"Unable to read data from '{_amiiboJsonPath}': {exception}");
{ isValid = false;
Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}"); }
// Neither local file is not valid JSON, close window. if (!isValid)
await ShowInfoDialog(); {
Close(); return null;
}
} }
return amiiboJson; return amiiboJson;
@@ -299,8 +285,8 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
AmiiboJson? amiiboJson; AmiiboJson? amiiboJson;
if (CommandLineState.OnlyLocalAmiibo) if (RyujinxOptions.Shared.OnlyLocalAmiibo)
amiiboJson = await ReadLocalJsonFileAsync(); amiiboJson = ReadLocalJsonFile();
else else
amiiboJson = await GetMostRecentAmiiboListOrDefaultJson(); amiiboJson = await GetMostRecentAmiiboListOrDefaultJson();

View File

@@ -184,7 +184,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_controller = 0; _controller = 0;
} }
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1) if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1)
{ {
ControllerType controller = Controllers[_controller].Type; ControllerType controller = Controllers[_controller].Type;
@@ -467,7 +467,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
IsModified = true; IsModified = true;
RevertChanges(); RevertChanges();
FindPairedDeviceInConfigFile(); FindPairedDeviceInConfigFile();
_isChangeTrackingActive = true; // Enable configuration change tracking _isChangeTrackingActive = true; // Enable configuration change tracking
} }
@@ -521,17 +521,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1) if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1)
{ {
int controllerIndex = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType); Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
// Avalonia bug: setting a newly instanced ComboBox to 0
// causes the selected item to show up blank
// Workaround: set the box to 1 and then 0
if (controllerIndex == 0)
{
Controller = 1;
}
Controller = controllerIndex;
} }
else else
{ {
@@ -586,7 +576,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
DeviceList.Clear(); DeviceList.Clear();
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled])); Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
int controllerNumber = 0;
foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds) foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds)
{ {
using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
@@ -603,7 +593,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (gamepad != null) if (gamepad != null)
{ {
int controllerNumber = 0;
string name = GetUniqueGamepadName(gamepad, ref controllerNumber); string name = GetUniqueGamepadName(gamepad, ref controllerNumber);
Devices.Add((DeviceType.Controller, id, name)); Devices.Add((DeviceType.Controller, id, name));
} }
@@ -961,10 +950,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad
Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId); Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId);
_isLoaded = false;
LoadConfiguration();
LoadDevice(); LoadDevice();
_isLoaded = true; LoadConfiguration();
OnPropertyChanged(); OnPropertyChanged();
IsModified = false; IsModified = false;

View File

@@ -2117,8 +2117,6 @@ namespace Ryujinx.Ava.UI.ViewModels
} }
); );
public string FavoriteStatusText => SelectedApplication?.Favorite == false ? LocaleManager.Instance[LocaleKeys.GameListContextMenuAddToFavorites] : LocaleManager.Instance[LocaleKeys.GameListContextMenuRemoveFromFavorites];
public static RelayCommand<MainWindowViewModel> CreateApplicationShortcut { get; } = public static RelayCommand<MainWindowViewModel> CreateApplicationShortcut { get; } =
Commands.CreateConditional<MainWindowViewModel>(vm => vm?.SelectedApplication != null, Commands.CreateConditional<MainWindowViewModel>(vm => vm?.SelectedApplication != null,
viewModel => ShortcutHelper.CreateAppShortcut( viewModel => ShortcutHelper.CreateAppShortcut(

View File

@@ -5,7 +5,6 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.Apple;
using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL3; using Ryujinx.Audio.Backends.SDL3;
using Ryujinx.Audio.Backends.SoundIo; using Ryujinx.Audio.Backends.SoundIo;
@@ -278,7 +277,6 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsOpenAlEnabled { get; set; } public bool IsOpenAlEnabled { get; set; }
public bool IsSoundIoEnabled { get; set; } public bool IsSoundIoEnabled { get; set; }
public bool IsSDL3Enabled { get; set; } public bool IsSDL3Enabled { get; set; }
public bool IsAudioToolboxEnabled { get; set; }
public bool IsCustomResolutionScaleActive => _resolutionScale == 4; public bool IsCustomResolutionScaleActive => _resolutionScale == 4;
public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr; public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr;
@@ -526,14 +524,12 @@ namespace Ryujinx.Ava.UI.ViewModels
IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported; IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported;
IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported; IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported;
IsSDL3Enabled = SDL3HardwareDeviceDriver.IsSupported; IsSDL3Enabled = SDL3HardwareDeviceDriver.IsSupported;
IsAudioToolboxEnabled = OperatingSystem.IsMacOS() && AppleHardwareDeviceDriver.IsSupported;
await Dispatcher.UIThread.InvokeAsync(() => await Dispatcher.UIThread.InvokeAsync(() =>
{ {
OnPropertyChanged(nameof(IsOpenAlEnabled)); OnPropertyChanged(nameof(IsOpenAlEnabled));
OnPropertyChanged(nameof(IsSoundIoEnabled)); OnPropertyChanged(nameof(IsSoundIoEnabled));
OnPropertyChanged(nameof(IsSDL3Enabled)); OnPropertyChanged(nameof(IsSDL3Enabled));
OnPropertyChanged(nameof(IsAudioToolboxEnabled));
}); });
} }

View File

@@ -28,9 +28,6 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] [ObservableProperty]
public partial ObservableCollection<ProfileImageModel> Images { get; set; } public partial ObservableCollection<ProfileImageModel> Images { get; set; }
[ObservableProperty]
public partial bool FirmwareFound { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Color BackgroundColor { get; set; } = Colors.White; public partial Color BackgroundColor { get; set; } = Colors.White;
@@ -46,15 +43,17 @@ namespace Ryujinx.Ava.UI.ViewModels
}; };
} }
private int _selectedIndex = -1;
public int SelectedIndex public int SelectedIndex
{ {
get => _selectedIndex; get;
set set
{ {
_selectedIndex = value; field = value;
SelectedImage = value == -1 ? null : Images[value].Data;
SelectedImage = field == -1
? null
: Images[field].Data;
OnPropertyChanged(); OnPropertyChanged();
OnPropertyChanged(nameof(SelectedImage)); OnPropertyChanged(nameof(SelectedImage));
} }

View File

@@ -0,0 +1,10 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class UserProfileImageSelectorViewModel : BaseModel
{
[ObservableProperty]
public partial bool FirmwareFound { get; set; }
}
}

View File

@@ -1,7 +1,7 @@
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Linq;
namespace Ryujinx.Ava.UI.ViewModels namespace Ryujinx.Ava.UI.ViewModels
{ {
@@ -9,35 +9,20 @@ namespace Ryujinx.Ava.UI.ViewModels
{ {
public UserProfileViewModel() public UserProfileViewModel()
{ {
Profiles = new ObservableCollection<BaseModel>(); Profiles = [];
LostProfiles = new ObservableCollection<UserProfile>(); LostProfiles = [];
LostProfiles.CollectionChanged += LostProfilesChanged; IsEmpty = !LostProfiles.Any();
} }
public ObservableCollection<BaseModel> Profiles { get; } public ObservableCollection<BaseModel> Profiles { get; set; }
public ObservableCollection<UserProfile> LostProfiles { get; } public ObservableCollection<UserProfile> LostProfiles { get; set; }
public bool IsEmpty => LostProfiles.Count == 0; public bool IsEmpty { get; set; }
public void Dispose() public void Dispose()
{ {
LostProfiles.CollectionChanged -= LostProfilesChanged; GC.SuppressFinalize(this);
}
private void LostProfilesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(IsEmpty));
}
public void UpdateLostProfiles(ObservableCollection<UserProfile> newProfiles)
{
LostProfiles.Clear();
foreach (var profile in newProfiles)
LostProfiles.Add(profile);
OnPropertyChanged(nameof(IsEmpty));
} }
} }
} }

View File

@@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private readonly AccountManager _accountManager; private readonly AccountManager _accountManager;
public string SaveManagerTitle => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UserProfiles_SaveManagerTitle, _accountManager.LastOpenedUser.Name); public string SaveManagerHeading => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SaveManagerHeading, _accountManager.LastOpenedUser.Name, _accountManager.LastOpenedUser.UserId);
public UserSaveManagerViewModel(AccountManager accountManager) public UserSaveManagerViewModel(AccountManager accountManager)
{ {

View File

@@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.Views.Dialog
{ {
PrimaryButtonText = string.Empty, PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty, SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
Content = new AboutView { ViewModel = viewModel } Content = new AboutView { ViewModel = viewModel }
}; };

View File

@@ -136,7 +136,7 @@
<MenuItem <MenuItem
Command="{Binding ManageProfiles}" Command="{Binding ManageProfiles}"
Padding="0" Padding="0"
Header="{ext:Locale UserProfiles_MenuBarOptions_OpenUserProfiles}" Header="{ext:Locale MenuBarOptionsManageUserProfiles}"
Icon="{ext:Icon fa-solid fa-user}" Icon="{ext:Icon fa-solid fa-user}"
IsEnabled="{Binding EnableNonGameRunningControls}" IsEnabled="{Binding EnableNonGameRunningControls}"
Classes="withCheckbox"> Classes="withCheckbox">

View File

@@ -46,9 +46,6 @@
<ComboBoxItem <ComboBoxItem
IsEnabled="{Binding IsSDL3Enabled}" IsEnabled="{Binding IsSDL3Enabled}"
Content="{ext:Locale SettingsTabSystemAudioBackendSDL3}" /> Content="{ext:Locale SettingsTabSystemAudioBackendSDL3}" />
<ComboBoxItem
IsEnabled="{Binding IsAudioToolboxEnabled}"
Content="{ext:Locale SettingsTabSystemAudioBackendAudioToolbox}" />
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal"> <StackPanel Margin="10,0,0,0" Orientation="Horizontal">

View File

@@ -8,102 +8,107 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
mc:Ignorable="d" Margin="0"
MinWidth="500" MinWidth="500"
Padding="0"
mc:Ignorable="d"
Focusable="True" Focusable="True"
x:DataType="models:TempProfile"> x:DataType="models:TempProfile">
<Grid Margin="0,10,0,0" ColumnDefinitions="Auto,*" RowDefinitions="*,Auto"> <Grid Margin="0" ColumnDefinitions="Auto,*" RowDefinitions="*,Auto">
<StackPanel Spacing="10"> <StackPanel
<TextBlock Text="{ext:Locale UserProfiles_NameLabel}" /> Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Text="{ext:Locale UserProfilesName}" />
<TextBox <TextBox
Name="NameBox" Name="NameBox"
Margin="0,0,0,5"
Width="300" Width="300"
HorizontalAlignment="Stretch"
MaxLength="{Binding MaxProfileNameLength}" MaxLength="{Binding MaxProfileNameLength}"
Watermark="{ext:Locale UserProfiles_ProfileNameSelectionWatermark}" Watermark="{ext:Locale ProfileNameSelectionWatermark}"
Text="{Binding Name}" /> Text="{Binding Name}" />
<TextBlock Name="IdText" Text="{ext:Locale UserProfiles_UserIdLabel}" /> <TextBlock Name="IdText" Text="{ext:Locale UserProfilesUserId}" />
<TextBox <TextBox
Name="IdLabel" Name="IdLabel"
Width="300" Width="300"
HorizontalAlignment="Stretch"
IsReadOnly="True" IsReadOnly="True"
Text="{Binding UserIdString}" /> Text="{Binding UserIdString}" />
</StackPanel> </StackPanel>
<Grid Grid.Column="1"> <StackPanel
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
Orientation="Vertical">
<Border <Border
Name="ImageBox"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}" BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1" BorderThickness="1">
VerticalAlignment="Bottom"
HorizontalAlignment="Right">
<Panel> <Panel>
<ui:SymbolIcon <ui:SymbolIcon
FontSize="70" FontSize="60"
Width="120" Width="96"
Height="120" Height="96"
Margin="0"
Foreground="{DynamicResource AppListHoverBackgroundColor}" Foreground="{DynamicResource AppListHoverBackgroundColor}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Symbol="Camera" /> Symbol="Camera" />
<Image <Image
Name="ProfileImage" Name="ProfileImage"
Width="120" Width="96"
Height="120" Height="96"
Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" /> Margin="0"
<Border HorizontalAlignment="Stretch"
Margin="2"
Height="27"
Width="27"
CornerRadius="17"
VerticalAlignment="Top" VerticalAlignment="Top"
HorizontalAlignment="Right" Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
Background="{DynamicResource ThemeContentBackgroundColor}">
<Button
Name="ProfileImageButton"
MaxHeight="27"
MaxWidth="27"
MinWidth="27"
MinHeight="27"
CornerRadius="17"
Padding="0">
<ui:SymbolIcon Symbol="Edit" />
<Button.Flyout>
<MenuFlyout Placement="Bottom">
<MenuItem
Header="{ext:Locale UserProfiles_ProfileImage_Import}"
Icon="{ext:Icon fa-solid fa-image}"
Click="Import_OnClick" />
<MenuItem
Header="{ext:Locale UserProfiles_ProfileImage_SelectAvatar}"
Icon="{ext:Icon fa-solid fa-floppy-disk}"
Click="SelectFirmwareImage_OnClick" />
</MenuFlyout>
</Button.Flyout>
</Button>
</Border>
</Panel> </Panel>
</Border> </Border>
</Grid> </StackPanel>
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
HorizontalAlignment="Left"
Orientation="Horizontal" Orientation="Horizontal"
Margin="0,30,0,0" Margin="0 24 0 0"
Spacing="10"> Spacing="10">
<Button MinWidth="50" Width="50" Click="BackButton_Click"> <Button
Width="50"
MinWidth="50"
Click="BackButton_Click">
<ui:SymbolIcon Symbol="Back" /> <ui:SymbolIcon Symbol="Back" />
</Button> </Button>
</StackPanel> </StackPanel>
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Orientation="Horizontal" Orientation="Horizontal"
Margin="0,30,0,0" Margin="0 24 0 0"
Spacing="10"> Spacing="10">
<Button Name="DeleteButton" Click="DeleteButton_Click"> <Button
<TextBlock Text="{ext:Locale UserProfiles_ButtonDelete}" /> Name="DeleteButton"
Click="DeleteButton_Click">
<TextBlock Text="{ext:Locale UserProfilesDelete}" />
</Button> </Button>
<Button Name="SaveButton" Click="SaveButton_Click"> <Button
<TextBlock Text="{ext:Locale UserProfiles_ButtonSave}" /> Name="ChangePictureButton"
Click="ChangePictureButton_Click">
<TextBlock Text="{ext:Locale UserProfilesChangeProfileImage}" />
</Button>
<Button
Name="AddPictureButton"
Click="ChangePictureButton_Click">
<TextBlock Text="{ext:Locale UserProfilesSetProfileImage}" />
</Button>
<Button
Name="SaveButton"
Click="SaveButton_Click">
<TextBlock Text="{ext:Locale UserProfilesSetProfileImage}" />
</Button> </Button>
</StackPanel> </StackPanel>
</Grid> </Grid>

View File

@@ -1,10 +1,6 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.VisualTree;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation; using FluentAvalonia.UI.Navigation;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
@@ -12,10 +8,6 @@ using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.FileSystem;
using SkiaSharp;
using System.Collections.Generic;
using System.IO;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile; using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
namespace Ryujinx.Ava.UI.Views.User namespace Ryujinx.Ava.UI.Views.User
@@ -23,75 +15,90 @@ namespace Ryujinx.Ava.UI.Views.User
public partial class UserEditorView : RyujinxControl<TempProfile> public partial class UserEditorView : RyujinxControl<TempProfile>
{ {
private NavigationDialogHost _parent; private NavigationDialogHost _parent;
private ContentManager _contentManager;
private UserProfile _profile; private UserProfile _profile;
private TempProfile _tempProfile;
private bool _isNewUser; private bool _isNewUser;
public static uint MaxProfileNameLength => 0x20; public static uint MaxProfileNameLength => 0x20;
public bool IsDeletable => _profile.UserId != AccountManager.DefaultUserId; public bool IsDeletable => _profile.UserId != AccountManager.DefaultUserId;
public string UserEditorTitle => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UserProfiles_UserEditorTitle, _profile.Name);
public UserEditorView() public UserEditorView()
{ {
InitializeComponent(); InitializeComponent();
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct); AddHandler(Frame.NavigatedToEvent, (s, e) =>
{
NavigatedTo(e);
}, RoutingStrategies.Direct);
} }
private void NavigatedTo(NavigationEventArgs arg) private void NavigatedTo(NavigationEventArgs arg)
{ {
if (!Program.PreviewerDetached) if (Program.PreviewerDetached)
return;
if (arg.NavigationMode == NavigationMode.New)
{ {
(NavigationDialogHost parent, UserProfile profile, bool isNewUser) = switch (arg.NavigationMode)
((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter; {
case NavigationMode.New:
(NavigationDialogHost parent, UserProfile profile, bool isNewUser) = ((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
_isNewUser = isNewUser;
_profile = profile;
ViewModel = new TempProfile(_profile);
_parent = parent; _parent = parent;
_profile = profile; break;
_isNewUser = isNewUser; }
DataValidationErrors.ClearErrors(NameBox); ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - " +
DataValidationErrors.ClearErrors(ImageBox); $"{(_isNewUser ? LocaleManager.Instance[LocaleKeys.UserEditorTitleCreate] : LocaleManager.Instance[LocaleKeys.UserEditorTitle])}";
ImageBox.Bind(Border.BorderBrushProperty, new DynamicResourceExtension("AppListHoverBackgroundColor"));
ViewModel = new TempProfile(_profile); AddPictureButton.IsVisible = _isNewUser;
_tempProfile = ViewModel; ChangePictureButton.IsVisible = !_isNewUser;
IdLabel.IsVisible = _profile != null;
_contentManager = _parent.ContentManager; IdText.IsVisible = _profile != null;
ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null; if (!_isNewUser && IsDeletable)
{
DeleteButton.IsVisible = true;
}
else
{
DeleteButton.IsVisible = false;
}
} }
((ContentDialog)_parent.Parent).Title =
$"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - " +
$"{(_isNewUser ? LocaleManager.Instance[LocaleKeys.UserProfiles_UserEditorTitleNewUser] : UserEditorTitle)}";
bool hasProfile = _profile != null;
IdLabel.IsVisible = hasProfile;
IdText.IsVisible = hasProfile;
DeleteButton.IsVisible = !_isNewUser && IsDeletable;
} }
private async void BackButton_Click(object sender, RoutedEventArgs e) private async void BackButton_Click(object sender, RoutedEventArgs e)
{ {
bool hasUnsavedChanges = if (_isNewUser)
_isNewUser
? (ViewModel.Name != string.Empty || ViewModel.Image != null)
: (_profile.Name != ViewModel.Name || _profile.Image != ViewModel.Image);
if (hasUnsavedChanges)
{ {
bool confirm = await ContentDialogHelper.CreateChoiceDialog( if (ViewModel.Name != string.Empty || ViewModel.Image != null)
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesTitle], {
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesMessage], if (await ContentDialogHelper.CreateChoiceDialog(
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesSubMessage]); LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
if (confirm) LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
{
_parent?.GoBack();
}
}
else
{
_parent?.GoBack(); _parent?.GoBack();
}
} }
else else
{ {
_parent?.GoBack(); if (_profile.Name != ViewModel.Name || _profile.Image != ViewModel.Image)
{
if (await ContentDialogHelper.CreateChoiceDialog(
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
{
_parent?.GoBack();
}
}
else
{
_parent?.GoBack();
}
} }
} }
@@ -103,28 +110,18 @@ namespace Ryujinx.Ava.UI.Views.User
private void SaveButton_Click(object sender, RoutedEventArgs e) private void SaveButton_Click(object sender, RoutedEventArgs e)
{ {
DataValidationErrors.ClearErrors(NameBox); DataValidationErrors.ClearErrors(NameBox);
DataValidationErrors.ClearErrors(ImageBox);
ImageBox.Bind(Border.BorderBrushProperty, new DynamicResourceExtension("AppListHoverBackgroundColor"));
bool nameEmpty = string.IsNullOrWhiteSpace(ViewModel.Name); if (string.IsNullOrWhiteSpace(ViewModel.Name))
bool imageMissing = ViewModel.Image == null;
if (nameEmpty && imageMissing)
{
DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfiles_EmptyNameError]));
DataValidationErrors.SetError(ImageBox, new DataValidationException(""));
ImageBox.BorderBrush = Brush.Parse("#ff99a4");
return;
}
else if (nameEmpty)
{ {
DataValidationErrors.SetError(NameBox,new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfiles_EmptyNameError])); DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfileEmptyNameError]));
return; return;
} }
else if (imageMissing)
if (ViewModel.Image == null)
{ {
DataValidationErrors.SetError(ImageBox, new DataValidationException("")); _parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, ViewModel));
ImageBox.BorderBrush = Brush.Parse("#ff99a4");
return; return;
} }
@@ -133,7 +130,6 @@ namespace Ryujinx.Ava.UI.Views.User
_profile.Name = ViewModel.Name; _profile.Name = ViewModel.Name;
_profile.Image = ViewModel.Image; _profile.Image = ViewModel.Image;
_profile.UpdateState(); _profile.UpdateState();
_parent.AccountManager.SetUserName(_profile.UserId, _profile.Name); _parent.AccountManager.SetUserName(_profile.UserId, _profile.Name);
_parent.AccountManager.SetUserImage(_profile.UserId, _profile.Image); _parent.AccountManager.SetUserImage(_profile.UserId, _profile.Image);
} }
@@ -149,80 +145,17 @@ namespace Ryujinx.Ava.UI.Views.User
_parent?.GoBack(); _parent?.GoBack();
} }
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e) public void SelectProfileImage()
{ {
if (ViewModel.FirmwareFound) _parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, ViewModel));
{
_parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _tempProfile));
}
} }
private async void Import_OnClick(object sender, RoutedEventArgs e) private void ChangePictureButton_Click(object sender, RoutedEventArgs e)
{ {
var window = (Window)this.GetVisualRoot()!; if (_profile != null || _isNewUser)
var result = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{ {
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_SupportedImageFormatDialogTitle], SelectProfileImage();
AllowMultiple = false,
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
{
Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp"],
AppleUniformTypeIdentifiers = ["public.jpeg", "public.png", "com.microsoft.bmp"],
MimeTypes = ["image/jpeg", "image/png", "image/bmp"],
},
new("JPG")
{
Patterns = ["*.jpg"],
AppleUniformTypeIdentifiers = ["public.jpeg"],
MimeTypes = ["image/jpeg"],
},
new("JPEG")
{
Patterns = ["*.jpeg"],
AppleUniformTypeIdentifiers = ["public.jpeg"],
MimeTypes = ["image/jpeg"],
},
new("PNG")
{
Patterns = ["*.png"],
AppleUniformTypeIdentifiers = ["public.png"],
MimeTypes = ["image/png"],
},
new("BMP")
{
Patterns = ["*.bmp"],
AppleUniformTypeIdentifiers = ["com.microsoft.bmp"],
MimeTypes = ["image/bmp"],
},
},
});
if (result.Count == 0 || DataContext is not TempProfile temp)
return;
temp.Image = ProcessProfileImage(File.ReadAllBytes(result[0].Path.LocalPath));
if (_profile != null)
_profile.Image = temp.Image;
}
private static byte[] ProcessProfileImage(byte[] buffer)
{
using SKBitmap bitmap = SKBitmap.Decode(buffer);
SKBitmap resizedBitmap = bitmap.Resize(new SKImageInfo(256, 256), SKFilterQuality.High);
using MemoryStream streamJpg = new();
if (resizedBitmap != null)
{
using SKImage image = SKImage.FromBitmap(resizedBitmap);
using SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100);
dataJpeg.SaveTo(streamJpg);
} }
return streamJpg.ToArray();
} }
} }
} }

View File

@@ -1,81 +1,73 @@
<UserControl <UserControl
x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
d:DesignHeight="350"
d:DesignWidth="578"
mc:Ignorable="d" mc:Ignorable="d"
Width="528" Width="528"
Focusable="True" d:DesignWidth="578"
x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel"> d:DesignHeight="350"
x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel"
Focusable="True">
<Design.DataContext> <Design.DataContext>
<viewModels:UserFirmwareAvatarSelectorViewModel /> <viewModels:UserFirmwareAvatarSelectorViewModel />
</Design.DataContext> </Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto,Auto"> <Grid
<Border Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto,Auto">
<ListBox
Grid.Row="1" Grid.Row="1"
Padding="2.5" BorderThickness="0"
BorderThickness="1" SelectedIndex="{Binding SelectedIndex}"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}" Height="400"
CornerRadius="5" ItemsSource="{Binding Images}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"> VerticalAlignment="Center">
<ListBox <ListBox.ItemsPanel>
Grid.Row="1" <ItemsPanelTemplate>
Background="Transparent" <WrapPanel
SelectedIndex="{Binding SelectedIndex}" Orientation="Horizontal"
Height="400" Margin="0"
ItemsSource="{Binding Images}"> HorizontalAlignment="Center" />
<ListBox.ItemsPanel> </ItemsPanelTemplate>
<ItemsPanelTemplate> </ListBox.ItemsPanel>
<WrapPanel <ListBox.Styles>
Orientation="Horizontal" <Style Selector="ListBoxItem">
Margin="0" <Setter Property="CornerRadius" Value="4" />
HorizontalAlignment="Center" /> <Setter Property="Width" Value="85" />
</ItemsPanelTemplate> <Setter Property="MaxWidth" Value="85" />
</ListBox.ItemsPanel> <Setter Property="MinWidth" Value="85" />
<ListBox.Styles> </Style>
<Style Selector="ListBoxItem"> <Style Selector="ListBoxItem /template/ Rectangle#SelectionIndicator">
<Setter Property="CornerRadius" Value="4" /> <Setter Property="MinHeight" Value="70" />
<Setter Property="Width" Value="85" /> </Style>
<Setter Property="MaxWidth" Value="85" /> </ListBox.Styles>
<Setter Property="MinWidth" Value="85" /> <ListBox.ItemTemplate>
</Style> <DataTemplate>
<Style Selector="ListBoxItem /template/ Rectangle#SelectionIndicator"> <Panel
<Setter Property="MinHeight" Value="70" /> Background="{Binding BackgroundColor}"
<Setter Property="IsVisible" Value="False" /> Margin="5">
</Style> <Image Source="{Binding Data, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator"> </Panel>
<Setter Property="IsVisible" Value="True" /> </DataTemplate>
</Style> </ListBox.ItemTemplate>
</ListBox.Styles> </ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
<Panel
Background="{Binding BackgroundColor}"
Margin="5">
<Image Source="{Binding Data, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
</Panel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<StackPanel <StackPanel
Grid.Row="3" Grid.Row="3"
Orientation="Horizontal" Orientation="Horizontal"
Spacing="10" Spacing="10"
Margin="0,30,0,0" Margin="0 24 0 0"
HorizontalAlignment="Left"> HorizontalAlignment="Left">
<Button <Button
Width="50" Width="50"
MinWidth="50" MinWidth="50"
Height="37" Height="35"
Click="GoBack"> Click="GoBack">
<ui:SymbolIcon Symbol="Back" /> <ui:SymbolIcon Symbol="Back" />
</Button> </Button>
@@ -84,7 +76,7 @@
Grid.Row="3" Grid.Row="3"
Orientation="Horizontal" Orientation="Horizontal"
Spacing="10" Spacing="10"
Margin="0,30,0,0" Margin="0 24 0 0"
HorizontalAlignment="Right"> HorizontalAlignment="Right">
<ui:ColorPickerButton <ui:ColorPickerButton
FlyoutPlacement="Top" FlyoutPlacement="Top"
@@ -103,10 +95,10 @@
</ui:ColorPickerButton.Styles> </ui:ColorPickerButton.Styles>
</ui:ColorPickerButton> </ui:ColorPickerButton>
<Button <Button
Height="37" Content="{ext:Locale AvatarChoose}"
Click="ChooseButton_OnClick"> Height="35"
<TextBlock Text="{ext:Locale UserProfiles_ButtonChooseAvatar}" /> Name="ChooseButton"
</Button> Click="ChooseButton_OnClick" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -1,15 +1,12 @@
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation; using FluentAvalonia.UI.Navigation;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls; using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using SkiaSharp; using SkiaSharp;
using System.IO; using System.IO;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Views.User namespace Ryujinx.Ava.UI.Views.User
{ {
@@ -17,53 +14,44 @@ namespace Ryujinx.Ava.UI.Views.User
{ {
private NavigationDialogHost _parent; private NavigationDialogHost _parent;
private TempProfile _profile; private TempProfile _profile;
public ContentManager ContentManager { get; private set; }
public UserFirmwareAvatarSelectorView()
{
InitializeComponent();
ViewModel = new UserFirmwareAvatarSelectorViewModel();
DataContext = ViewModel;
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct);
}
public UserFirmwareAvatarSelectorView(ContentManager contentManager) public UserFirmwareAvatarSelectorView(ContentManager contentManager)
{ {
ContentManager = contentManager; ContentManager = contentManager;
InitializeComponent(); InitializeComponent();
ViewModel = new UserFirmwareAvatarSelectorViewModel(); }
DataContext = ViewModel;
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct); public UserFirmwareAvatarSelectorView()
{
InitializeComponent();
AddHandler(Frame.NavigatedToEvent, (s, e) =>
{
NavigatedTo(e);
}, RoutingStrategies.Direct);
} }
private void NavigatedTo(NavigationEventArgs arg) private void NavigatedTo(NavigationEventArgs arg)
{ {
if (!Program.PreviewerDetached) if (Program.PreviewerDetached)
return;
if (arg.NavigationMode != NavigationMode.New)
return;
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
ContentManager = _parent.ContentManager;
((ContentDialog)_parent.Parent).Title =
$"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - " +
$"{LocaleManager.Instance[LocaleKeys.UserProfiles_SelectAvatarTitle]}";
ViewModel.SelectedIndex = -1;
_ = Task.Run(() =>
{ {
bool found = ContentManager.GetCurrentFirmwareVersion() != null; if (arg.NavigationMode == NavigationMode.New)
Dispatcher.UIThread.Post(() =>
{ {
ViewModel.FirmwareFound = found; (_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
}); ContentManager = _parent.ContentManager;
}); if (Program.PreviewerDetached)
{
ViewModel = new UserFirmwareAvatarSelectorViewModel();
}
DataContext = ViewModel;
}
}
} }
public ContentManager ContentManager { get; private set; }
private void GoBack(object sender, RoutedEventArgs e) private void GoBack(object sender, RoutedEventArgs e)
{ {
_parent.GoBack(); _parent.GoBack();
@@ -71,31 +59,32 @@ namespace Ryujinx.Ava.UI.Views.User
private void ChooseButton_OnClick(object sender, RoutedEventArgs e) private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
{ {
if (ViewModel.SelectedImage == null) if (ViewModel.SelectedImage != null)
return;
using MemoryStream streamJpg = new();
using SKBitmap bitmap = SKBitmap.Decode(ViewModel.SelectedImage);
using SKBitmap newBitmap = new(bitmap.Width, bitmap.Height);
using (SKCanvas canvas = new(newBitmap))
{ {
canvas.Clear(new SKColor( using MemoryStream streamJpg = new();
ViewModel.BackgroundColor.R, using SKBitmap bitmap = SKBitmap.Decode(ViewModel.SelectedImage);
ViewModel.BackgroundColor.G, using SKBitmap newBitmap = new(bitmap.Width, bitmap.Height);
ViewModel.BackgroundColor.B,
ViewModel.BackgroundColor.A));
canvas.DrawBitmap(bitmap, 0, 0);
}
using (SKImage image = SKImage.FromBitmap(newBitmap)) using (SKCanvas canvas = new(newBitmap))
using (SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100)) {
{ canvas.Clear(new SKColor(
dataJpeg.SaveTo(streamJpg); ViewModel.BackgroundColor.R,
} ViewModel.BackgroundColor.G,
ViewModel.BackgroundColor.B,
ViewModel.BackgroundColor.A));
canvas.DrawBitmap(bitmap, 0, 0);
}
_profile.Image = streamJpg.ToArray(); using (SKImage image = SKImage.FromBitmap(newBitmap))
_parent.GoBack(); using (SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100))
{
dataJpeg.SaveTo(streamJpg);
}
_profile.Image = streamJpg.ToArray();
_parent.GoBack();
}
} }
} }
} }

View File

@@ -0,0 +1,57 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:viewModles="clr-namespace:Ryujinx.Ava.UI.ViewModels"
Focusable="True"
mc:Ignorable="d"
x:Class="Ryujinx.Ava.UI.Views.User.UserProfileImageSelectorView"
x:DataType="viewModles:UserProfileImageSelectorViewModel"
Width="500"
d:DesignWidth="500">
<Design.DataContext>
<viewModles:UserProfileImageSelectorViewModel />
</Design.DataContext>
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Center" RowDefinitions="Auto,70,Auto">
<TextBlock
Grid.Row="0"
TextWrapping="Wrap"
HorizontalAlignment="Left"
TextAlignment="Start"
Text="{ext:Locale ProfileImageSelectionNote}" />
<StackPanel
Grid.Row="2"
Spacing="10"
HorizontalAlignment="Left"
Orientation="Horizontal">
<Button
Width="50"
MinWidth="50"
Click="GoBack">
<ui:SymbolIcon Symbol="Back" />
</Button>
</StackPanel>
<StackPanel
Grid.Row="2"
Spacing="10"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
Name="Import"
Click="Import_OnClick">
<TextBlock Text="{ext:Locale ProfileImageSelectionImportImage}" />
</Button>
<Button
Name="SelectFirmwareImage"
IsEnabled="{Binding FirmwareFound}"
Click="SelectFirmwareImage_OnClick">
<TextBlock Text="{ext:Locale ProfileImageSelectionSelectAvatar}" />
</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,118 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.VisualTree;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Navigation;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
using SkiaSharp;
using System.Collections.Generic;
using System.IO;
namespace Ryujinx.Ava.UI.Views.User
{
public partial class UserProfileImageSelectorView : RyujinxControl<UserProfileImageSelectorViewModel>
{
private ContentManager _contentManager;
private NavigationDialogHost _parent;
private TempProfile _profile;
public UserProfileImageSelectorView()
{
InitializeComponent();
AddHandler(Frame.NavigatedToEvent, (s, e) =>
{
NavigatedTo(e);
}, RoutingStrategies.Direct);
}
private void NavigatedTo(NavigationEventArgs arg)
{
if (Program.PreviewerDetached)
{
switch (arg.NavigationMode)
{
case NavigationMode.New:
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
_contentManager = _parent.ContentManager;
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.ProfileImageSelectionHeader]}";
if (Program.PreviewerDetached)
{
DataContext = ViewModel = new UserProfileImageSelectorViewModel();
ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null;
}
break;
case NavigationMode.Back:
if (_profile.Image != null)
{
_parent.GoBack();
}
break;
}
}
}
private async void Import_OnClick(object sender, RoutedEventArgs e)
{
IReadOnlyList<IStorageFile> result = await ((Window)this.GetVisualRoot()!).StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
AllowMultiple = false,
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
{
Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp"],
AppleUniformTypeIdentifiers = ["public.jpeg", "public.png", "com.microsoft.bmp"],
MimeTypes = ["image/jpeg", "image/png", "image/bmp"],
},
},
});
if (result.Count > 0)
{
_profile.Image = ProcessProfileImage(File.ReadAllBytes(result[0].Path.LocalPath));
_parent.GoBack();
}
}
private void GoBack(object sender, RoutedEventArgs e)
{
_parent.GoBack();
}
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
{
if (ViewModel.FirmwareFound)
{
_parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _profile));
}
}
private static byte[] ProcessProfileImage(byte[] buffer)
{
using SKBitmap bitmap = SKBitmap.Decode(buffer);
SKBitmap resizedBitmap = bitmap.Resize(new SKImageInfo(256, 256), SKFilterQuality.High);
using MemoryStream streamJpg = new();
if (resizedBitmap != null)
{
using SKImage image = SKImage.FromBitmap(resizedBitmap);
using SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100);
dataJpeg.SaveTo(streamJpg);
}
return streamJpg.ToArray();
}
}
}

View File

@@ -1,64 +1,53 @@
<UserControl <UserControl
x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" mc:Ignorable="d"
d:DesignWidth="550" d:DesignWidth="550"
d:DesignHeight="450" d:DesignHeight="450"
mc:Ignorable="d"
Width="500" Width="500"
Height="400" Height="400"
Focusable="True" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
x:DataType="viewModels:UserProfileViewModel"> xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView"
x:DataType="viewModels:UserProfileViewModel"
Focusable="True">
<Design.DataContext> <Design.DataContext>
<viewModels:UserProfileViewModel /> <viewModels:UserProfileViewModel />
</Design.DataContext> </Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto"> <Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" RowDefinitions="*,Auto">
<Border <Border
CornerRadius="5" CornerRadius="5"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}" BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1" BorderThickness="1"
Grid.Row="0" Grid.Row="0">
Padding="2.5">
<Panel> <Panel>
<ListBox <ListBox
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding LostProfiles}"> ItemsSource="{Binding LostProfiles}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="10" />
<Setter Property="Margin" Value="0" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
<Setter Property="IsVisible" Value="False" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border <Border
Margin="2"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ClipToBounds="True" ClipToBounds="True"
CornerRadius="4"> CornerRadius="5">
<Grid Margin="0" ColumnDefinitions="*,Auto"> <Grid Margin="0" ColumnDefinitions="*,Auto">
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Margin="5"
Text="{Binding UserId}" Text="{Binding UserId}"
TextAlignment="Start" TextAlignment="Start"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<Button Grid.Column="1" <Button Grid.Column="1"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Margin="5" Click="Recover"
Command="{Binding Recover}" CommandParameter="{Binding}"
CommandParameter="{Binding}"> Content="{ext:Locale Recover}"/>
<TextBlock Text="{ext:Locale UserProfiles_RecoverProfile}" />
</Button>
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
@@ -67,12 +56,12 @@
<TextBlock <TextBlock
IsVisible="{Binding IsEmpty}" IsVisible="{Binding IsEmpty}"
TextAlignment="Center" TextAlignment="Center"
Text="{ext:Locale UserProfiles_RecoverProfile_EmptyList}"/> Text="{ext:Locale UserProfilesRecoverEmptyList}"/>
</Panel> </Panel>
</Border> </Border>
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
Margin="0,30,0,0" Margin="0 24 0 0"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
Width="50" Width="50"

View File

@@ -27,12 +27,11 @@ namespace Ryujinx.Ava.UI.Views.User
switch (arg.NavigationMode) switch (arg.NavigationMode)
{ {
case NavigationMode.New: case NavigationMode.New:
case NavigationMode.Back:
NavigationDialogHost parent = (NavigationDialogHost)arg.Parameter; NavigationDialogHost parent = (NavigationDialogHost)arg.Parameter;
_parent = parent; _parent = parent;
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfiles_RecoverLostProfiles]}"; ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfilesRecoverHeading]}";
break; break;
} }
@@ -43,5 +42,10 @@ namespace Ryujinx.Ava.UI.Views.User
{ {
_parent?.GoBack(); _parent?.GoBack();
} }
private void Recover(object sender, RoutedEventArgs e)
{
_parent?.RecoverLostAccounts();
}
} }
} }

View File

@@ -1,11 +1,10 @@
<UserControl <UserControl
x:Class="Ryujinx.Ava.UI.Views.User.UserSaveManagerView"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
@@ -14,6 +13,7 @@
d:DesignHeight="500" d:DesignHeight="500"
Height="450" Height="450"
Width="550" Width="550"
x:Class="Ryujinx.Ava.UI.Views.User.UserSaveManagerView"
x:DataType="viewModels:UserSaveManagerViewModel" x:DataType="viewModels:UserSaveManagerViewModel"
Focusable="True"> Focusable="True">
<Design.DataContext> <Design.DataContext>
@@ -22,9 +22,9 @@
<Grid RowDefinitions="Auto,*,Auto"> <Grid RowDefinitions="Auto,*,Auto">
<Grid <Grid
Grid.Row="0" Grid.Row="0"
Margin="0,0,0,5"
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*"> HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*">
<StackPanel <StackPanel
Margin="0,0,0,10"
Spacing="10" Spacing="10"
Orientation="Horizontal" Orientation="Horizontal"
HorizontalAlignment="Left" HorizontalAlignment="Left"
@@ -33,9 +33,9 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
MinWidth="100"> MinWidth="100">
<ComboBoxItem <ComboBoxItem
Content="{ext:Locale UserProfiles_ManageSaves_SortByName}" /> Content="{ext:Locale Name}" />
<ComboBoxItem <ComboBoxItem
Content="{ext:Locale UserProfiles_ManageSaves_SortBySize}" /> Content="{ext:Locale Size}" />
<ComboBox.Styles> <ComboBox.Styles>
<Style Selector="ContentControl#ContentPresenter"> <Style Selector="ContentControl#ContentPresenter">
<Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Left" />
@@ -46,9 +46,9 @@
HorizontalContentAlignment="Left" HorizontalContentAlignment="Left"
MinWidth="150"> MinWidth="150">
<ComboBoxItem <ComboBoxItem
Content="{ext:Locale UserProfiles_ManageSaves_SortOrderAscending}" /> Content="{ext:Locale OrderAscending}" />
<ComboBoxItem <ComboBoxItem
Content="{ext:Locale UserProfiles_ManageSaves_SortOrderDescending}" /> Content="{ext:Locale OrderDescending}" />
<ComboBox.Styles> <ComboBox.Styles>
<Style Selector="ContentControl#ContentPresenter"> <Style Selector="ContentControl#ContentPresenter">
<Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="HorizontalAlignment" Value="Left" />
@@ -59,18 +59,18 @@
<Grid <Grid
Grid.Column="1" Grid.Column="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Margin="20,0,0,10" ColumnDefinitions="Auto,*"> Margin="10,0, 0, 0" ColumnDefinitions="Auto,*">
<TextBlock Text="{ext:Locale Search}" VerticalAlignment="Center" />
<TextBox <TextBox
Margin="5,0,0,0" Margin="10,0,0,0"
Grid.Column="1" Grid.Column="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{Binding Search}" Text="{Binding Search}" />
Watermark="{ext:Locale UserProfiles_ManageSaves_Search}" />
</Grid> </Grid>
</Grid> </Grid>
<Border <Border
Grid.Row="1" Grid.Row="1"
Padding="2.5" Margin="0,5"
BorderThickness="1" BorderThickness="1"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}" BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
CornerRadius="5" CornerRadius="5"
@@ -84,7 +84,7 @@
<ListBox.Styles> <ListBox.Styles>
<Style Selector="ListBoxItem"> <Style Selector="ListBoxItem">
<Setter Property="Padding" Value="10" /> <Setter Property="Padding" Value="10" />
<Setter Property="Margin" Value="0" /> <Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="4" /> <Setter Property="CornerRadius" Value="4" />
</Style> </Style>
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator"> <Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
@@ -168,7 +168,7 @@
</Border> </Border>
<StackPanel <StackPanel
Grid.Row="2" Grid.Row="2"
Margin="0,30,0,0" Margin="0 24 0 0"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
Width="50" Width="50"

View File

@@ -55,7 +55,7 @@ namespace Ryujinx.Ava.UI.Views.User
} }
DataContext = ViewModel = new UserSaveManagerViewModel(_accountManager); DataContext = ViewModel = new UserSaveManagerViewModel(_accountManager);
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - {ViewModel.SaveManagerTitle}"; ((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {ViewModel.SaveManagerHeading}";
Task.Run(LoadSaves); Task.Run(LoadSaves);
} }
@@ -127,8 +127,8 @@ namespace Ryujinx.Ava.UI.Views.User
{ {
if (button.DataContext is SaveModel saveModel) if (button.DataContext is SaveModel saveModel)
{ {
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.UserProfiles_DeleteSaveNote], UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DeleteUserSave],
LocaleManager.Instance[LocaleKeys.UserProfiles_IrreversibleActionNote], LocaleManager.Instance[LocaleKeys.IrreversibleActionNote],
LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo], LocaleManager.Instance[LocaleKeys.InputDialogNo],
string.Empty); string.Empty);

View File

@@ -4,39 +4,43 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
d:DesignHeight="450" d:DesignHeight="450"
MinWidth="500"
d:DesignWidth="800" d:DesignWidth="800"
mc:Ignorable="d" mc:Ignorable="d"
MinWidth="500"
Focusable="True" Focusable="True"
x:DataType="viewModels:UserProfileViewModel"> x:DataType="viewModels:UserProfileViewModel">
<Design.DataContext> <Design.DataContext>
<viewModels:UserProfileViewModel /> <viewModels:UserProfileViewModel />
</Design.DataContext> </Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto">
<Border Padding="-3" BorderThickness="0"> <Border
CornerRadius="5"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1">
<ListBox <ListBox
HorizontalAlignment="Center" MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalAlignment="Center" VerticalAlignment="Center"
SelectionChanged="ProfilesList_SelectionChanged"
Background="Transparent" Background="Transparent"
ItemsSource="{Binding Profiles}" ItemsSource="{Binding Profiles}">
SelectionChanged="ProfilesList_SelectionChanged">
<ListBox.ItemsPanel> <ListBox.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<WrapPanel <WrapPanel
HorizontalAlignment="Center" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
Orientation="Horizontal"/> Orientation="Horizontal"/>
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ListBox.ItemsPanel> </ListBox.ItemsPanel>
<ListBox.Styles> <ListBox.Styles>
<Style Selector="ListBoxItem"> <Style Selector="ListBoxItem">
<Setter Property="Margin" Value="0" /> <Setter Property="Margin" Value="5 5 0 5" />
<Setter Property="CornerRadius" Value="5" /> <Setter Property="CornerRadius" Value="5" />
</Style> </Style>
<Style Selector="Rectangle#SelectionIndicator"> <Style Selector="Rectangle#SelectionIndicator">
@@ -50,7 +54,6 @@
PointerEntered="Grid_PointerEntered" PointerEntered="Grid_PointerEntered"
PointerExited="Grid_OnPointerExited"> PointerExited="Grid_OnPointerExited">
<Border <Border
Margin="5"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
ClipToBounds="True" ClipToBounds="True"
@@ -62,20 +65,18 @@
<Image <Image
Width="96" Width="96"
Height="96" Height="96"
Margin="0,0,0,10"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Top" VerticalAlignment="Top"
Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" /> Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
<TextBlock <TextBlock
Text="{Binding Name}" HorizontalAlignment="Stretch"
Height="30"
MaxWidth="90" MaxWidth="90"
Text="{Binding Name}"
TextAlignment="Center" TextAlignment="Center"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap" TextWrapping="Wrap"
MaxLines="2" /> TextTrimming="CharacterEllipsis"
MaxLines="2"
Margin="5" />
</StackPanel> </StackPanel>
</Border> </Border>
<Border <Border
@@ -88,10 +89,10 @@
Background="{DynamicResource ThemeContentBackgroundColor}" Background="{DynamicResource ThemeContentBackgroundColor}"
IsVisible="{Binding IsPointerOver}"> IsVisible="{Binding IsPointerOver}">
<Button <Button
MinHeight="24"
MinWidth="24"
MaxHeight="24" MaxHeight="24"
MaxWidth="24" MaxWidth="24"
MinHeight="24"
MinWidth="24"
CornerRadius="12" CornerRadius="12"
Padding="0" Padding="0"
Click="EditUser"> Click="EditUser">
@@ -103,8 +104,8 @@
<DataTemplate <DataTemplate
DataType="viewModels:BaseModel"> DataType="viewModels:BaseModel">
<Panel <Panel
Height="146" Height="118"
Width="106"> Width="96">
<Button <Button
MinWidth="50" MinWidth="50"
MinHeight="50" MinHeight="50"
@@ -118,6 +119,11 @@
Click="AddUser"> Click="AddUser">
<ui:SymbolIcon Symbol="Add" /> <ui:SymbolIcon Symbol="Add" />
</Button> </Button>
<Panel.Styles>
<Style Selector="Panel">
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}"/>
</Style>
</Panel.Styles>
</Panel> </Panel>
</DataTemplate> </DataTemplate>
</ListBox.DataTemplates> </ListBox.DataTemplates>
@@ -125,29 +131,27 @@
</Border> </Border>
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
Margin="0,30,0,0" Margin="0 24 0 0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Orientation="Horizontal" Orientation="Horizontal"
Spacing="10"> Spacing="10">
<Button <Button
Click="ManageSaves"> Click="ManageSaves">
<TextBlock Text="{ext:Locale UserProfiles_ManageSaves}" /> <TextBlock Text="{ext:Locale UserProfilesManageSaves}" />
</Button> </Button>
<Button <Button
Click="RecoverLostAccounts"> Click="RecoverLostAccounts">
<TextBlock <TextBlock Text="{ext:Locale UserProfilesRecoverLostAccounts}" />
Text="{ext:Locale UserProfiles_RecoverLostProfiles}"
ToolTip.Tip="{ext:Locale UserProfiles_RecoverLostProfiles_ToolTip}" />
</Button> </Button>
</StackPanel> </StackPanel>
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
Margin="0,30,0,0" Margin="0 24 0 0"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
Click="Close"> Click="Close">
<TextBlock Text="{ext:Locale UserProfiles_ButtonClose}" /> <TextBlock Text="{ext:Locale UserProfilesClose}" />
</Button> </Button>
</StackPanel> </StackPanel>
</Grid> </Grid>

View File

@@ -40,7 +40,7 @@ namespace Ryujinx.Ava.UI.Views.User
if (arg.NavigationMode == NavigationMode.Back) if (arg.NavigationMode == NavigationMode.Back)
{ {
((ContentDialog)_parent.Parent).Title = LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]; ((ContentDialog)_parent.Parent).Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle];
} }
DataContext = ViewModel; DataContext = ViewModel;

View File

@@ -139,16 +139,11 @@ namespace Ryujinx.Ava.UI.Windows
Executor.ExecuteBackgroundAsync(async () => Executor.ExecuteBackgroundAsync(async () =>
{ {
await ShowIntelMacWarningAsync(); await ShowIntelMacWarningAsync();
if (CommandLineState.FirmwareToInstallPathArg.TryGet(out FilePath fwPath)) if (RyujinxOptions.Shared.FirmwareToInstallPath.TryGet(out FilePath fwPath))
{ {
if (fwPath is { ExistsAsFile: true, Extension: "xci" or "zip" } || fwPath.ExistsAsDirectory) await Dispatcher.UIThread.InvokeAsync(() =>
{ ViewModel.HandleFirmwareInstallation(fwPath));
await Dispatcher.UIThread.InvokeAsync(() => RyujinxOptions.Shared.FirmwareToInstallPath = default;
ViewModel.HandleFirmwareInstallation(fwPath));
CommandLineState.FirmwareToInstallPathArg = default;
}
else
Logger.Notice.Print(LogClass.UI, "Invalid firmware type provided. Path must be a directory, or a .zip or .xci file.");
} }
}); });
} }
@@ -278,7 +273,7 @@ namespace Ryujinx.Ava.UI.Windows
// Consider removing this at some point in the future when we don't need to worry about old saves. // Consider removing this at some point in the future when we don't need to worry about old saves.
VirtualFileSystem.FixExtraData(LibHacHorizonManager.RyujinxClient); VirtualFileSystem.FixExtraData(LibHacHorizonManager.RyujinxClient);
AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient, CommandLineState.Profile); AccountManager = new AccountManager(LibHacHorizonManager.RyujinxClient, RyujinxOptions.Shared.Profile);
VirtualFileSystem.ReloadKeySet(); VirtualFileSystem.ReloadKeySet();
@@ -406,7 +401,7 @@ namespace Ryujinx.Ava.UI.Windows
await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys)); await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys));
} }
if (!Updater.CanUpdate() || CommandLineState.HideAvailableUpdates) if (!Updater.CanUpdate() || RyujinxOptions.Shared.HideAvailableUpdates)
return; return;
switch (ConfigurationState.Instance.UpdateCheckerType.Value) switch (ConfigurationState.Instance.UpdateCheckerType.Value)

View File

@@ -1,221 +0,0 @@
using Gommon;
using Ryujinx.Common.Logging;
using System.Collections.Generic;
namespace Ryujinx.Ava.Utilities
{
public static class CommandLineState
{
public static string[] Arguments { get; private set; }
public static int CountArguments { get; private set; }
public static bool? OverrideDockedMode { get; private set; }
public static bool? OverrideHardwareAcceleration { get; private set; }
public static string OverrideGraphicsBackend { get; private set; }
public static string OverrideBackendThreading { get; private set; }
public static string OverrideBackendThreadingAfterReboot { get; private set; }
public static string OverridePPTC { get; private set; }
public static string OverrideMemoryManagerMode { get; private set; }
public static string OverrideSystemRegion { get; private set; }
public static string OverrideSystemLanguage { get; private set; }
public static string OverrideHideCursor { get; private set; }
public static string BaseDirPathArg { get; private set; }
public static string RenderDocCaptureTitleFormat { get; private set; } =
"{EmuVersion}\n{GuestName} {GuestVersion} {GuestTitleId} {GuestArch}";
public static Optional<FilePath> FirmwareToInstallPathArg { get; set; }
public static string Profile { get; private set; }
public static string LaunchPathArg { get; private set; }
public static string LaunchApplicationId { get; private set; }
public static bool StartFullscreenArg { get; private set; }
public static bool HideAvailableUpdates { get; private set; }
public static bool OnlyLocalAmiibo { get; private set; }
public static void ParseArguments(string[] args)
{
List<string> arguments = [];
// Parse Arguments.
for (int i = 0; i < args.Length; ++i)
{
string arg = args[i];
if (arg.Contains('-') || arg.Contains("--"))
{
CountArguments++;
}
switch (arg)
{
case "-r":
case "--root-data-dir":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
BaseDirPathArg = args[++i];
arguments.Add(arg);
arguments.Add(args[i]);
break;
case "-rdct":
case "--rd-capture-title-format":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
RenderDocCaptureTitleFormat = args[++i];
arguments.Add(arg);
arguments.Add(args[i]);
break;
case "--install-firmware":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
FirmwareToInstallPathArg = new FilePath(args[++i]);
arguments.Add(arg);
arguments.Add(args[i]);
break;
case "-p":
case "--profile":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
Profile = args[++i];
arguments.Add(arg);
arguments.Add(args[i]);
break;
case "-f":
case "--fullscreen":
StartFullscreenArg = true;
arguments.Add(arg);
break;
case "-g":
case "--graphics-backend":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideGraphicsBackend = args[++i];
break;
case "--backend-threading":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideBackendThreading = args[++i];
break;
case "--bt":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideBackendThreadingAfterReboot = args[++i];
break;
case "--pptc":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverridePPTC = args[++i];
break;
case "-la":
case "--local-only-amiibo":
OnlyLocalAmiibo = true;
break;
case "-m":
case "--memory-manager-mode":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideMemoryManagerMode = args[++i];
break;
case "--system-region":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideSystemRegion = args[++i];
break;
case "--system-language":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideSystemLanguage = args[++i];
break;
case "-i":
case "--application-id":
LaunchApplicationId = args[++i];
break;
case "--docked-mode":
OverrideDockedMode = true;
break;
case "--handheld-mode":
OverrideDockedMode = false;
break;
case "--hide-cursor":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideHideCursor = args[++i];
break;
case "--hide-updates":
HideAvailableUpdates = true;
break;
case "--software-gui":
OverrideHardwareAcceleration = false;
break;
default:
LaunchPathArg = arg;
break;
}
}
Arguments = arguments.ToArray();
}
}
}

View File

@@ -0,0 +1,58 @@
using CommandLine;
using Gommon;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using Error = Gommon.Error;
namespace Ryujinx.Ava.Utilities
{
public partial class RyujinxOptions
{
public static RyujinxOptions Shared { get; private set; }
// ReSharper disable once UnusedMethodReturnValue.Global
public static Result Read(string[] args, out RyujinxOptions options)
{
options = null;
args = PatchLegacyArgumentNames(args);
ParserResult<RyujinxOptions> parseResult =
Parser.ParseArguments<RyujinxOptions>(args);
if (parseResult is NotParsed<RyujinxOptions>)
return Result.Fail;
options = Shared = parseResult.Value;
return parseResult.Value.Init(args);
}
private static readonly Lazy<Parser> _parser = new(() => new Parser(settings =>
{
settings.HelpWriter = Logger.WriterProxy;
settings.CaseInsensitiveEnumValues = true;
settings.CaseSensitive = false;
settings.MaximumDisplayWidth -= (int)(settings.MaximumDisplayWidth * 0.175);
}));
public static Parser Parser => _parser.Value;
private static readonly Dictionary<string, string> _legacyArgs = new()
{
{ "-rdct", "--rd-capture-title-format" },
{ "-la", "--local-only-amiibo" }
};
public static string[] PatchLegacyArgumentNames(string[] args)
{
for (int i = 0; i < args.Length; i++)
args[i] = Patch(args[i]);
return args;
string Patch(string arg) => _legacyArgs.TryGetValue(arg, out string newArgName) ? newArgName : arg;
}
}
}

View File

@@ -0,0 +1,148 @@
using Avalonia.Controls;
using CommandLine;
using Gommon;
using Ryujinx.Ava.Systems.Configuration.System;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
namespace Ryujinx.Ava.Utilities
{
public partial class RyujinxOptions
{
public string[] InputArguments { get; private set; }
public bool? DockedModeOverride { get; private set; }
public bool? HardwareAccelerationOverride { get; private set; }
public Optional<FilePath> FirmwareToInstallPath { get; set; }
// Ideally I'd use an enum parse, like --docked-mode=Handheld,
// but I want to maintain backwards compatibility with shortcuts made a long time ago, as best we can.
public Result Init(string[] args)
{
InputArguments = args;
{
// Docked Mode Override
if (DockedMode && HandheldMode)
{
return Result.MessageFailure(
"Cannot be in both docked and handheld mode at the same time; choose only one.");
}
if (DockedMode) DockedModeOverride = true;
if (HandheldMode) DockedModeOverride = false;
}
{
// Hardware Acceleration Override
if (SoftwareGui)
{
HardwareAccelerationOverride = false;
}
}
FirmwareToInstallPath = Optional.Of(FirmwareToInstallPathRaw)
.Convert(x => new FilePath(x))
.OnlyIf(fp =>
{
bool result = fp is { ExistsAsFile: true, Extension: "xci" or "zip" } || fp.ExistsAsDirectory;
if (!result)
{
Logger.Notice.PrintMsg(LogClass.UI,
"Invalid firmware type provided. Path must be a directory, or a .zip or .xci file.");
}
return result;
});
return Result.Success;
}
[Option("docked-mode", Required = false, Default = false,
HelpText = "Launch the game in Docked mode. Causes an error if used in tandem with --handheld-mode.")]
public bool DockedMode { get; set; }
[Option("handheld-mode", Required = false, Default = false,
HelpText = "Launch the game in Handheld mode. Causes an error if used in tandem with --docked-mode.")]
public bool HandheldMode { get; set; }
[Option("software-gui", Required = false, Default = false,
HelpText = "Disables hardware-accelerated rendering for Avalonia. Required for launching with RenderDoc.")]
public bool SoftwareGui { get; set; }
[Option('g', "graphics-backend", Required = false, Default = null,
HelpText = "Select the Graphics backend to use when launching.")]
public GraphicsBackend? GraphicsBackendOverride { get; set; }
[Option("backend-threading", Required = false, Default = null,
HelpText = "Select the Graphics backend threading option to use when launching.")]
public BackendThreading? BackendThreadingOverride { get; set; }
[Option("bt", Required = false, Default = null, Hidden = true)]
public BackendThreading? BackendThreadingOverrideAfterReboot { get; set; }
[Option("pptc", Required = false, Default = null,
HelpText = "Enable/disable PPTC regardless of your settings when launching.")]
public string PptcOverride { get; set; }
[Option('m', "memory-manager-mode", Required = false, Default = null,
HelpText = "Select the memory manager mode to use when launching.")]
public MemoryManagerMode? MemoryManagerModeOverride { get; set; }
[Option("system-region", Required = false, Default = null,
HelpText = "Select the Region to use for the emulated Switch when launching.")]
public Region? SystemRegionOverride { get; set; }
[Option("system-language", Required = false, Default = null,
HelpText = "Select the Language to use for the emulated Switch when launching.")]
public Language? SystemLanguageOverride { get; set; }
[Option("hide-cursor", Required = false, Default = null,
HelpText = "Select the cursor hiding strategy to use when launching.")]
public HideCursorMode? HideCursorOverride { get; set; }
[Option('r', "root-data-dir", Required = false, Default = null,
HelpText = "Select the folder to use for all of your Ryujinx save data, configs, etc.")]
public string EmuDataBaseDirPath { get; set; }
[Option("rd-capture-title-format", Required = false,
HelpText =
"Set the format string used for RenderDoc Capture titles when using the Start/Stop Capture buttons in Ryujinx.",
Default = "{EmuVersion}\n{GuestName} {GuestVersion} {GuestTitleId} {GuestArch}")]
public string RenderDocCaptureTitleFormat { get; set; }
[Option("install-firmware", Required = false, Default = null,
HelpText =
"Specify a file path containing Switch firmware to install immediately after starting. Must be a directory or a .zip or .xci file.")]
public string FirmwareToInstallPathRaw { get; set; }
[Option('p', "profile", Required = false, Default = null,
HelpText = "The profile name to open the application with. Defaults to your last used profile.")]
public string Profile { get; set; }
[Option('i', "application-id", Required = false, Default = null,
HelpText = "Specify which application ID out of the specified content archive path to launch.")]
public string LaunchApplicationId { get; set; }
[Option('f', "fullscreen", Required = false, Default = false,
HelpText = "Start the emulator in fullscreen mode.")]
public bool StartFullscreen { get; set; }
[Option("hide-updates", Required = false, Default = false, HelpText = "Hides update prompt/notification.")]
public bool HideAvailableUpdates { get; set; }
[Option("local-only-amiibo", Required = false, Default = false,
HelpText = "Only use the local Amiibo cache; do not update it even if there is an update.")]
public bool OnlyLocalAmiibo { get; set; }
[Option("core-dumps", Required = false, Default = false,
HelpText = "Enable coredumps on Linux platforms. They are disabled by default.")]
public bool CoreDumpsEnabled { get; set; }
[Value(0, Default = null, Required = false,
HelpText =
"The Nintendo Switch application content archive to launch immediately after starting, if desired.")]
public string LaunchPath { get; set; }
}
}

View File

@@ -126,10 +126,10 @@ namespace Ryujinx.Ava.Utilities
// args are first defined as a list, for easier adjustments in the future // args are first defined as a list, for easier adjustments in the future
List<string> argsList = []; List<string> argsList = [];
if (!string.IsNullOrEmpty(CommandLineState.BaseDirPathArg)) if (!string.IsNullOrEmpty(RyujinxOptions.Shared.EmuDataBaseDirPath))
{ {
argsList.Add("--root-data-dir"); argsList.Add("--root-data-dir");
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\""); argsList.Add($"\"{RyujinxOptions.Shared.EmuDataBaseDirPath}\"");
} }
if (!string.IsNullOrEmpty(config)) if (!string.IsNullOrEmpty(config))

View File

@@ -34,7 +34,7 @@ namespace Ryujinx.Ava.Utilities
string titleIdSection = $"({activeProcess.ProgramIdText.ToUpper()})"; string titleIdSection = $"({activeProcess.ProgramIdText.ToUpper()})";
string titleArchSection = activeProcess.Is64Bit ? "(64-bit)" : "(32-bit)"; string titleArchSection = activeProcess.Is64Bit ? "(64-bit)" : "(32-bit)";
return CommandLineState.RenderDocCaptureTitleFormat return RyujinxOptions.Shared.RenderDocCaptureTitleFormat
.ReplaceIgnoreCase("{EmuVersion}", applicationVersion) .ReplaceIgnoreCase("{EmuVersion}", applicationVersion)
.ReplaceIgnoreCase("{GuestName}", titleNameSection) .ReplaceIgnoreCase("{GuestName}", titleNameSection)
.ReplaceIgnoreCase("{GuestVersion}", titleVersionSection) .ReplaceIgnoreCase("{GuestVersion}", titleVersionSection)