Compare commits

..

13 Commits

Author SHA1 Message Date
GreemDev
37c911b962 Treat Intel Mac builds as dirty builds to disable the updater. 2026-03-14 15:58:51 -05:00
Coxxs
9cae62096a HLE: Implement CreateContextForSystem (ryubing/ryujinx!285)
See merge request ryubing/ryujinx!285
2026-03-14 13:57:49 -05:00
BowedCascade
648b609ebb Add restart emulation command (ryubing/ryujinx!276)
See merge request ryubing/ryujinx!276
2026-03-14 13:56:20 -05:00
BowedCascade
5ae86fc493 Fix keys file overwrite on installation and method name typo (ryubing/ryujinx!268)
See merge request ryubing/ryujinx!268
2026-03-14 13:52:58 -05:00
KeatonTheBot
6f90e47a73 UI: Restore FluentAvaloniaUI package, disable animations on app initialization (ryubing/ryujinx!256)
See merge request ryubing/ryujinx!256
2026-03-14 13:48:59 -05:00
sh0inx
ac5f9857e2 HLE: Implement IHidServer IsSixAxisSensorAtRest (ryubing/ryujinx!228)
See merge request ryubing/ryujinx!228
2026-03-14 13:25:55 -05:00
KeatonTheBot
4b42087bd4 Linux: Fix file picker not launching from disabling core dumps (ryubing/ryujinx!249)
See merge request ryubing/ryujinx!249
2026-03-06 19:04:42 -06:00
EscoDev
80cbf5d1fc Fix incorrect save button locale in user editor (ryubing/ryujinx!280)
See merge request ryubing/ryujinx!280
2026-03-01 15:48:29 -06:00
LotP
cc6d2dc162 fix nacp language buffer (ryubing/ryujinx!281)
See merge request ryubing/ryujinx!281
2026-02-25 13:58:31 -06:00
Daenorth
4ebc318da5 Add new RPC images (ryubing/ryujinx!279)
See merge request ryubing/ryujinx!279
2026-02-23 20:57:19 -06:00
KeatonTheBot
00dad0a5e2 Windows ARM (win-arm64) build now launches with trimming (ryubing/ryujinx!277)
See merge request ryubing/ryujinx!277
2026-02-21 20:10:22 -06:00
Joshua de Reeper
b70e2e44cb NFC Mifare Manager (ryubing/ryujinx!270)
See merge request ryubing/ryujinx!270
2026-02-21 05:45:00 -06:00
sh0inx
012d1d6886 Fixed spelling in LocalesValidationTask.cs (ryubing/ryujinx!269)
See merge request ryubing/ryujinx!269
2026-02-21 04:37:02 -06:00
66 changed files with 2892 additions and 1594 deletions

View File

@@ -3,13 +3,13 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.6" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.6" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.6" />
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.6.2" />
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.6.2" />
<PackageVersion Include="Avalonia" Version="11.3.12" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.12" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.12" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.12" />
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.9.4" />
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.9.4" />
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
@@ -22,7 +22,7 @@
<PackageVersion Include="Concentus" Version="2.2.2" />
<PackageVersion Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageVersion Include="DynamicData" Version="9.4.1" />
<PackageVersion Include="FluentAvaloniaUI.NoAnim" Version="2.4.0-build3" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.5.0" />
<PackageVersion Include="Humanizer" Version="2.14.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
@@ -41,7 +41,7 @@
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.2-build3" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.129" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
<PackageVersion Include="Gommon" Version="2.8.0.1" />
@@ -56,7 +56,6 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageVersion Include="SPB" Version="0.0.4-build32" />
<PackageVersion Include="System.IO.Hashing" Version="9.0.2" />
<PackageVersion Include="System.Management" Version="9.0.2" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
</ItemGroup>
</Project>
</Project>

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

@@ -107,12 +107,12 @@ namespace Ryujinx.BuildValidationTasks
{
locale.Translations[langCode] = string.Empty;
Console.WriteLine(
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
$"Language '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
}
else
{
Console.WriteLine(
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
$"Language '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
}
}

View File

@@ -10,7 +10,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" />
<PackageReference Include="MsgPack.Cli" />
<PackageReference Include="System.Management" />
<PackageReference Include="Humanizer" />
<PackageReference Include="Gommon" />
</ItemGroup>

View File

@@ -184,6 +184,7 @@ namespace Ryujinx.Common
"01001b300b9be000", // Diablo III: Eternal Collection
"010027400cdc6000", // Divinity Original 2 - Definitive Edition
"01008c8012920000", // Dying Light Platinum Edition
"0100d11013e6a000", // Eschatos
"01001cc01b2d4000", // Goat Simulator 3
"01003620068ea000", // Hand of Fate 2
"0100f7e00c70e000", // Hogwarts Legacy
@@ -193,9 +194,15 @@ namespace Ryujinx.Common
"0100d71004694000", // Minecraft
"01007430037f6000", // Monopoly
"0100853015e86000", // No Man's Sky
"0100f85014ed0000", // No More Heroes
"0100463014ed4000", // No More Heroes 2
"0100e570094e8000", // Owlboy
"01007bb017812000", // Portal
"0100abd01785c000", // Portal 2
"01009f100bc52000", // Psikyo Collection 1
"01009d400c4a8000", // Psikyo Collection 2
"01008e200c5c2000", // Muse Dash
"01005ff002e2a000", // Rayman Legends
"01007820196a6000", // Red Dead Redemption
"0100e8300a67a000", // Risk
"01002f7013224000", // Rune Factory 5

View File

@@ -22,10 +22,11 @@ namespace Ryujinx.Common.Utilities
}
// "dumpable" attribute of the calling process
private const int PR_GET_DUMPABLE = 3;
private const int PR_SET_DUMPABLE = 4;
[DllImport("libc", SetLastError = true)]
private static extern int prctl(int option, int arg2);
[LibraryImport("libc", SetLastError = true)]
private static partial int prctl(int option, int arg2);
public static void SetCoreDumpable(bool dumpable)
{
@@ -36,5 +37,13 @@ namespace Ryujinx.Common.Utilities
Debug.Assert(result == 0);
}
}
// Use the below line to display dumpable status in the console:
// Console.WriteLine($"{OsUtils.IsCoreDumpable()}");
public static bool IsCoreDumpable()
{
int result = prctl(PR_GET_DUMPABLE, 0);
return result == 1;
}
}
}

View File

@@ -488,6 +488,8 @@ namespace Ryujinx.HLE.FileSystem
if (keyPaths.Length is 0)
throw new FileNotFoundException($"Directory '{keysSource}' contained no '.keys' files.");
List<string> failedFiles = new();
foreach (string filePath in keyPaths)
{
try
@@ -497,17 +499,20 @@ namespace Ryujinx.HLE.FileSystem
catch (Exception e)
{
Logger.Error?.Print(LogClass.Application, e.Message);
failedFiles.Add(Path.GetFileName(filePath));
continue;
}
string destPath = Path.Combine(installDirectory, Path.GetFileName(filePath));
if (File.Exists(destPath))
File.Delete(destPath);
File.Copy(filePath, destPath, true);
}
if (failedFiles.Count > 0)
{
throw new InvalidOperationException($"Failed to install the following key files: {string.Join(", ", failedFiles)}");
}
return;
}
@@ -518,8 +523,6 @@ namespace Ryujinx.HLE.FileSystem
FileInfo info = new(keysSource);
using FileStream file = File.OpenRead(keysSource);
if (info.Extension is not ".keys")
throw new InvalidFirmwarePackageException("Input file extension is not .keys");
@@ -534,10 +537,6 @@ namespace Ryujinx.HLE.FileSystem
string dest = Path.Combine(installDirectory, info.Name);
if (File.Exists(dest))
File.Delete(dest);
// overwrite: true seems to not work on its own? https://github.com/Ryubing/Issues/issues/189
File.Copy(keysSource, dest, true);
}
@@ -1059,7 +1058,7 @@ namespace Ryujinx.HLE.FileSystem
}
}
public static bool AreKeysAlredyPresent(string pathToCheck)
public static bool AreKeysAlreadyPresent(string pathToCheck)
{
string[] fileNames = ["prod.keys", "title.keys", "console.keys", "dev.keys"];
foreach (string file in fileNames)

View File

@@ -20,6 +20,7 @@ using Ryujinx.HLE.HOS.Services.Mii;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
using Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager;
using Ryujinx.HLE.HOS.Services.Nv;
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
using Ryujinx.HLE.HOS.Services.Pcv.Bpc;
@@ -66,6 +67,8 @@ namespace Ryujinx.HLE.HOS
internal List<NfpDevice> NfpDevices { get; private set; }
internal List<NfcDevice> NfcDevices { get; private set; }
internal SmRegistry SmRegistry { get; private set; }
internal ServerBase SmServer { get; private set; }
@@ -132,6 +135,7 @@ namespace Ryujinx.HLE.HOS
PerformanceState = new PerformanceState();
NfpDevices = [];
NfcDevices = [];
// Note: This is not really correct, but with HLE of services, the only memory
// region used that is used is Application, so we can use the other ones for anything.
@@ -372,6 +376,15 @@ namespace Ryujinx.HLE.HOS
}
}
public void ScanSkylander(int nfcDeviceId, byte[] data)
{
if (NfcDevices[nfcDeviceId].State == NfcDeviceState.SearchingForTag)
{
NfcDevices[nfcDeviceId].State = NfcDeviceState.TagFound;
NfcDevices[nfcDeviceId].Data = data;
}
}
public bool SearchingForAmiibo(out int nfpDeviceId)
{
nfpDeviceId = default;
@@ -389,6 +402,53 @@ namespace Ryujinx.HLE.HOS
return false;
}
public bool SearchingForSkylander(out int nfcDeviceId)
{
nfcDeviceId = default;
for (int i = 0; i < NfcDevices.Count; i++)
{
if (NfcDevices[i].State == NfcDeviceState.SearchingForTag)
{
nfcDeviceId = i;
return true;
}
}
return false;
}
public bool HasSkylander(out int nfcDeviceId)
{
nfcDeviceId = default;
for (int i = 0; i < NfcDevices.Count; i++)
{
if (NfcDevices[i].State == NfcDeviceState.TagFound)
{
nfcDeviceId = i;
return true;
}
}
return false;
}
public void RemoveSkylander()
{
for (int i = 0; i < NfcDevices.Count; i++)
{
if (NfcDevices[i].State == NfcDeviceState.TagFound)
{
NfcDevices[i].State = NfcDeviceState.Initialized;
NfcDevices[i].SignalDeactivate();
Thread.Sleep(100); // NOTE: Simulate skylander scanning delay.
}
}
}
public void SignalDisplayResolutionChange()
{
DisplayResolutionChangeEvent.ReadableEvent.Signal();

View File

@@ -56,6 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
_activeCount = 0;
JoyHold = NpadJoyHoldType.Vertical;
SixAxisActive = false;
}
internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player)
@@ -580,6 +581,24 @@ namespace Ryujinx.HLE.HOS.Services.Hid
return needUpdateRight;
}
public bool isAtRest(int playerNumber)
{
ref NpadInternalState currentNpad = ref _device.Hid.SharedMemory.Npads[playerNumber].InternalState;
ref SixAxisSensorState storage = ref GetSixAxisSensorLifo(ref currentNpad, false).GetCurrentEntryRef();
float acceleration = Math.Abs(storage.Acceleration.X)
+ Math.Abs(storage.Acceleration.Y)
+ Math.Abs(storage.Acceleration.Z);
float angularVelocity = Math.Abs(storage.AngularVelocity.X)
+ Math.Abs(storage.AngularVelocity.Y)
+ Math.Abs(storage.AngularVelocity.Z);
// TODO: check against config deadzone and add sensitivity setting
return ((acceleration <= 1.0F) && (angularVelocity <= 1.0F));
}
private void UpdateDisconnectedInputSixAxis(PlayerIndex index)
{

View File

@@ -602,19 +602,33 @@ namespace Ryujinx.HLE.HOS.Services.Hid
}
[CommandCmif(82)]
// IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAsRest
// IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAtRest
public ResultCode IsSixAxisSensorAtRest(ServiceCtx context)
{
int sixAxisSensorHandle = context.RequestData.ReadInt32();
// 4 byte struct w/ 4-byte alignment
// uint typeValue = (uint) sixAxisSensorHandle; // 0x0 0x4 TypeValue
// uint npadStyleIndex = (uint) sixAxisSensorHandle & 0xff; // 0x0 0x1 NpadStyleIndex
int playerNumber = (sixAxisSensorHandle << 8) & 0xff; // 0x1 0x1 PlayerNumber
// uint deviceIdx= ((uint) sixAxisSensorHandle << 16) & 0xff; // 0x2 0x1 DeviceIdx
// uint unknown = ((uint) sixAxisSensorHandle << 24) & 0xff;
// 32bit sign extension padding -> if = 0, + offset, else - offset
// npadStyleIndex = ((npadStyleIndex & 0x8000) == 0) ? npadStyleIndex | 0xFFFF0000 : npadStyleIndex & 0xFFFF0000;
// playerNumber = ((playerNumber & 0x8000) == 0) ? playerNumber | 0xFFFF0000 : playerNumber & 0xFFFF0000;
// deviceIdx = ((deviceIdx & 0x8000) == 0) ? deviceIdx | 0xFFFF0000 : deviceIdx & 0xFFFF0000;
// unknown = ((unknown & 0x8000) == 0) ? unknown | 0xFFFF0000 : unknown & 0xFFFF0000;
context.RequestData.BaseStream.Position += 4; // Padding
long appletResourceUserId = context.RequestData.ReadInt64();
bool isAtRest = true;
context.ResponseData.Write(isAtRest);
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, isAtRest });
// TODO: link to context.Device.Hid.Npads.SixAxisActive when properly implemented
// We currently do not support stopping or starting SixAxisTracking.
context.ResponseData.Write(context.Device.Hid.Npads.isAtRest(playerNumber));
return ResultCode.Success;
}
@@ -629,7 +643,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
context.ResponseData.Write(_isFirmwareUpdateAvailableForSixAxisSensor);
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _isFirmwareUpdateAvailableForSixAxisSensor });
return ResultCode.Success;
}

View File

@@ -1,8 +1,19 @@
using Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager;
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare
{
[Service("nfc:mf:u")]
class IUserManager : IpcService
{
public IUserManager(ServiceCtx context) { }
[CommandCmif(0)]
// CreateUserInterface() -> object<nn::nfc::mf::IUser>
public ResultCode CreateUserInterface(ServiceCtx context)
{
MakeObject(context, new IMifare());
return ResultCode.Success;
}
}
}

View File

@@ -0,0 +1,477 @@
using Ryujinx.Common.Memory;
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.Services.Hid.HidServer;
using Ryujinx.Horizon.Common;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
class IMifare : IpcService
{
private State _state;
private KEvent _availabilityChangeEvent;
private CancellationTokenSource _cancelTokenSource;
public IMifare()
{
_state = State.NonInitialized;
}
[CommandCmif(0)]
public ResultCode Initialize(ServiceCtx context)
{
_state = State.Initialized;
NfcDevice devicePlayer1 = new()
{
NpadIdType = NpadIdType.Player1,
Handle = HidUtils.GetIndexFromNpadIdType(NpadIdType.Player1),
State = NfcDeviceState.Initialized,
};
context.Device.System.NfcDevices.Add(devicePlayer1);
return ResultCode.Success;
}
[CommandCmif(1)]
public ResultCode Finalize(ServiceCtx context)
{
if (_state == State.Initialized)
{
_cancelTokenSource?.Cancel();
// NOTE: All events are destroyed here.
context.Device.System.NfcDevices.Clear();
_state = State.NonInitialized;
}
return ResultCode.Success;
}
[CommandCmif(2)]
public ResultCode GetListDevices(ServiceCtx context)
{
if (context.Request.RecvListBuff.Count == 0)
{
return ResultCode.WrongArgument;
}
ulong outputPosition = context.Request.RecvListBuff[0].Position;
ulong outputSize = context.Request.RecvListBuff[0].Size;
if (context.Device.System.NfcDevices.Count == 0)
{
return ResultCode.DeviceNotFound;
}
MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize);
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
context.Memory.Write(outputPosition + ((uint)i * sizeof(long)), (uint)context.Device.System.NfcDevices[i].Handle);
}
context.ResponseData.Write(context.Device.System.NfcDevices.Count);
return ResultCode.Success;
}
[CommandCmif(3)]
public ResultCode StartDetection(ServiceCtx context)
{
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
{
context.Device.System.NfcDevices[i].State = NfcDeviceState.SearchingForTag;
break;
}
}
_cancelTokenSource = new CancellationTokenSource();
Task.Run(() =>
{
while (true)
{
if (_cancelTokenSource.Token.IsCancellationRequested)
{
break;
}
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagFound)
{
context.Device.System.NfcDevices[i].SignalActivate();
Thread.Sleep(125); // NOTE: Simulate skylander scanning delay.
break;
}
}
}
}, _cancelTokenSource.Token);
return ResultCode.Success;
}
[CommandCmif(4)]
public ResultCode StopDetection(ServiceCtx context)
{
_cancelTokenSource?.Cancel();
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
{
context.Device.System.NfcDevices[i].State = NfcDeviceState.Initialized;
Array.Clear(context.Device.System.NfcDevices[i].Data);
context.Device.System.NfcDevices[i].SignalDeactivate();
break;
}
}
return ResultCode.Success;
}
[CommandCmif(5)]
public ResultCode ReadMifare(ServiceCtx context)
{
if (context.Request.ReceiveBuff.Count == 0 || context.Request.SendBuff.Count == 0)
{
return ResultCode.WrongArgument;
}
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
if (context.Device.System.NfcDevices.Count == 0)
{
return ResultCode.DeviceNotFound;
}
ulong outputPosition = context.Request.ReceiveBuff[0].Position;
ulong outputSize = context.Request.ReceiveBuff[0].Size;
MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize);
ulong inputPosition = context.Request.SendBuff[0].Position;
ulong inputSize = context.Request.SendBuff[0].Size;
byte[] readBlockParameter = new byte[inputSize];
context.Memory.Read(inputPosition, readBlockParameter);
var span = MemoryMarshal.Cast<byte, NfcMifareReadBlockParameter>(readBlockParameter);
var list = new List<NfcMifareReadBlockParameter>(span.Length);
foreach (var item in span)
list.Add(item);
Thread.Sleep(125 * list.Count); // NOTE: Simulate skylander scanning delay.
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
{
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved)
{
return ResultCode.TagNotFound;
}
else
{
for (int p = 0; p < list.Count; p++)
{
NfcMifareReadBlockData blockData = new()
{
SectorNumber = list[p].SectorNumber,
Reserved = new Array7<byte>(),
};
byte[] data = new byte[16];
switch (list[p].SectorKey.MifareCommand)
{
case NfcMifareCommand.NfcMifareCommand_Read:
case NfcMifareCommand.NfcMifareCommand_AuthA:
if (IsCurrentBlockKeyBlock(list[p].SectorNumber))
{
Array.Copy(context.Device.System.NfcDevices[i].Data, (16 * list[p].SectorNumber) + 6, data, 6, 4);
}
else
{
Array.Copy(context.Device.System.NfcDevices[i].Data, 16 * list[p].SectorNumber, data, 0, 16);
}
data.CopyTo(blockData.Data.AsSpan());
context.Memory.Write(outputPosition + ((uint)(p * Unsafe.SizeOf<NfcMifareReadBlockData>())), blockData);
break;
}
}
}
}
}
return ResultCode.Success;
}
[CommandCmif(6)]
public ResultCode WriteMifare(ServiceCtx context)
{
if (context.Request.SendBuff.Count == 0)
{
return ResultCode.WrongArgument;
}
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
if (context.Device.System.NfcDevices.Count == 0)
{
return ResultCode.DeviceNotFound;
}
ulong inputPosition = context.Request.SendBuff[0].Position;
ulong inputSize = context.Request.SendBuff[0].Size;
byte[] writeBlockParameter = new byte[inputSize];
context.Memory.Read(inputPosition, writeBlockParameter);
var span = MemoryMarshal.Cast<byte, NfcMifareWriteBlockParameter>(writeBlockParameter);
var list = new List<NfcMifareWriteBlockParameter>(span.Length);
foreach (var item in span)
list.Add(item);
Thread.Sleep(125 * list.Count); // NOTE: Simulate skylander scanning delay.
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
{
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved)
{
return ResultCode.TagNotFound;
}
else
{
for (int p = 0; p < list.Count; p++)
{
switch (list[p].SectorKey.MifareCommand)
{
case NfcMifareCommand.NfcMifareCommand_Write:
case NfcMifareCommand.NfcMifareCommand_AuthA:
list[p].Data.AsSpan().CopyTo(context.Device.System.NfcDevices[i].Data.AsSpan(list[p].SectorNumber * 16, 16));
break;
}
}
}
}
}
return ResultCode.Success;
}
[CommandCmif(7)]
public ResultCode GetTagInfo(ServiceCtx context)
{
ResultCode resultCode = ResultCode.Success;
if (context.Request.RecvListBuff.Count == 0)
{
return ResultCode.WrongArgument;
}
ulong outputPosition = context.Request.RecvListBuff[0].Position;
context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize((uint)Marshal.SizeOf<TagInfo>());
MemoryHelper.FillWithZeros(context.Memory, outputPosition, Marshal.SizeOf<TagInfo>());
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
if (context.Device.System.NfcDevices.Count == 0)
{
return ResultCode.DeviceNotFound;
}
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
{
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved)
{
resultCode = ResultCode.TagNotFound;
}
else
{
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagMounted || context.Device.System.NfcDevices[i].State == NfcDeviceState.TagFound)
{
TagInfo tagInfo = new()
{
UuidLength = 4,
Reserved1 = new Array21<byte>(),
Protocol = (uint)NfcProtocol.NfcProtocol_TypeA, // Type A Protocol
TagType = (uint)NfcTagType.NfcTagType_Mifare, // Mifare Type
Reserved2 = new Array6<byte>(),
};
byte[] uuid = new byte[4];
Array.Copy(context.Device.System.NfcDevices[i].Data, 0, uuid, 0, 4);
uuid.CopyTo(tagInfo.Uuid.AsSpan());
context.Memory.Write(outputPosition, tagInfo);
resultCode = ResultCode.Success;
}
else
{
resultCode = ResultCode.WrongDeviceState;
}
}
break;
}
}
return resultCode;
}
[CommandCmif(8)]
public ResultCode AttachActivateEvent(ServiceCtx context)
{
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle)
{
context.Device.System.NfcDevices[i].ActivateEvent = new KEvent(context.Device.System.KernelContext);
if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfcDevices[i].ActivateEvent.ReadableEvent, out int activateEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(activateEventHandle);
return ResultCode.Success;
}
}
return ResultCode.DeviceNotFound;
}
[CommandCmif(9)]
public ResultCode AttachDeactivateEvent(ServiceCtx context)
{
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle)
{
context.Device.System.NfcDevices[i].DeactivateEvent = new KEvent(context.Device.System.KernelContext);
if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfcDevices[i].DeactivateEvent.ReadableEvent, out int deactivateEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(deactivateEventHandle);
return ResultCode.Success;
}
}
return ResultCode.DeviceNotFound;
}
[CommandCmif(10)]
public ResultCode GetState(ServiceCtx context)
{
context.ResponseData.Write((int)_state);
return ResultCode.Success;
}
[CommandCmif(11)]
public ResultCode GetDeviceState(ServiceCtx context)
{
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle)
{
if (context.Device.System.NfcDevices[i].State > NfcDeviceState.Finalized)
{
throw new InvalidOperationException($"{nameof(context.Device.System.NfcDevices)} contains an invalid state for device {i}: {context.Device.System.NfcDevices[i].State}");
}
context.ResponseData.Write((uint)context.Device.System.NfcDevices[i].State);
return ResultCode.Success;
}
}
context.ResponseData.Write((uint)NfcDeviceState.Unavailable);
return ResultCode.DeviceNotFound;
}
[CommandCmif(12)]
public ResultCode GetNpadId(ServiceCtx context)
{
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
{
if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle)
{
context.ResponseData.Write((uint)HidUtils.GetNpadIdTypeFromIndex(context.Device.System.NfcDevices[i].Handle));
return ResultCode.Success;
}
}
return ResultCode.DeviceNotFound;
}
[CommandCmif(13)]
public ResultCode AttachAvailabilityChangeEvent(ServiceCtx context)
{
_availabilityChangeEvent = new KEvent(context.Device.System.KernelContext);
if (context.Process.HandleTable.GenerateHandle(_availabilityChangeEvent.ReadableEvent, out int availabilityChangeEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(availabilityChangeEventHandle);
return ResultCode.Success;
}
private bool IsCurrentBlockKeyBlock(byte block)
{
return ((block + 1) % 4) == 0;
}
}
}

View File

@@ -0,0 +1,21 @@
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Hid;
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
class NfcDevice
{
public KEvent ActivateEvent;
public KEvent DeactivateEvent;
public void SignalActivate() => ActivateEvent.ReadableEvent.Signal();
public void SignalDeactivate() => DeactivateEvent.ReadableEvent.Signal();
public NfcDeviceState State = NfcDeviceState.Unavailable;
public PlayerIndex Handle;
public NpadIdType NpadIdType;
public byte[] Data;
}
}

View File

@@ -0,0 +1,14 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
enum NfcMifareCommand : byte
{
NfcMifareCommand_Read = 0x30,
NfcMifareCommand_AuthA = 0x60,
NfcMifareCommand_AuthB = 0x61,
NfcMifareCommand_Write = 0xA0,
NfcMifareCommand_Transfer = 0xB0,
NfcMifareCommand_Decrement = 0xC0,
NfcMifareCommand_Increment = 0xC1,
NfcMifareCommand_Store = 0xC2,
}
}

View File

@@ -0,0 +1,13 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
[StructLayout(LayoutKind.Sequential, Size = 0x18)]
struct NfcMifareReadBlockData
{
public Array16<byte> Data;
public byte SectorNumber;
public Array7<byte> Reserved;
}
}

View File

@@ -0,0 +1,13 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
[StructLayout(LayoutKind.Sequential, Size = 0x18)]
struct NfcMifareReadBlockParameter
{
public byte SectorNumber;
public Array7<byte> Reserved;
public NfcSectorKey SectorKey;
}
}

View File

@@ -0,0 +1,14 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
[StructLayout(LayoutKind.Sequential, Size = 0x28)]
struct NfcMifareWriteBlockParameter
{
public Array16<byte> Data;
public byte SectorNumber;
public Array7<byte> Reserved;
public NfcSectorKey SectorKey;
}
}

View File

@@ -0,0 +1,11 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
enum NfcProtocol : byte
{
NfcProtocol_None = 0b_0000_0000,
NfcProtocol_TypeA = 0b_0000_0001, ///< ISO14443A
NfcProtocol_TypeB = 0b_0000_0010, ///< ISO14443B
NfcProtocol_TypeF = 0b_0000_0100, ///< Sony FeliCa
NfcProtocol_All = 0xFF,
}
}

View File

@@ -0,0 +1,16 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
struct NfcSectorKey
{
public NfcMifareCommand MifareCommand;
public byte Unknown;
public Array6<byte> Reserved1;
public Array6<byte> SectorKey;
public Array2<byte> Reserved2;
}
}

View File

@@ -0,0 +1,15 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
enum NfcTagType : byte
{
NfcTagType_None = 0b_0000_0000,
NfcTagType_Type1 = 0b_0000_0001, ///< ISO14443A RW. Topaz
NfcTagType_Type2 = 0b_0000_0010, ///< ISO14443A RW. Ultralight, NTAGX, ST25TN
NfcTagType_Type3 = 0b_0000_0100, ///< ISO14443A RW/RO. Sony FeliCa
NfcTagType_Type4A = 0b_0000_1000, ///< ISO14443A RW/RO. DESFire
NfcTagType_Type4B = 0b_0001_0000, ///< ISO14443B RW/RO. DESFire
NfcTagType_Type5 = 0b_0010_0000, ///< ISO15693 RW/RO. SLI, SLIX, ST25TV
NfcTagType_Mifare = 0b_0100_0000, ///< Mifare clasic. Skylanders
NfcTagType_All = 0xFF,
}
}

View File

@@ -0,0 +1,13 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
enum NfcDeviceState : byte
{
Initialized = 0,
SearchingForTag = 1,
TagFound = 2,
TagRemoved = 3,
TagMounted = 4,
Unavailable = 5,
Finalized = 6,
}
}

View File

@@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
enum State
{
NonInitialized,
Initialized,
}
}

View File

@@ -0,0 +1,16 @@
using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
[StructLayout(LayoutKind.Sequential, Size = 0x58)]
struct TagInfo
{
public Array10<byte> Uuid;
public byte UuidLength;
public Array21<byte> Reserved1;
public uint Protocol;
public uint TagType;
public Array6<byte> Reserved2;
}
}

View File

@@ -0,0 +1,17 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare
{
public enum ResultCode
{
ModuleId = 161,
ErrorCodeShift = 9,
Success = 0,
DeviceNotFound = (64 << ErrorCodeShift) | ModuleId, // 0x80A1
WrongArgument = (65 << ErrorCodeShift) | ModuleId, // 0x82A1
WrongDeviceState = (73 << ErrorCodeShift) | ModuleId, // 0x92A1
NfcDisabled = (80 << ErrorCodeShift) | ModuleId, // 0xA0A1
TagNotFound = (97 << ErrorCodeShift) | ModuleId, // 0xC2A1
MifareAccessError = (288 << ErrorCodeShift) | ModuleId, // 0x240a1
}
}

View File

@@ -17,7 +17,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl
public ISslService(ServiceCtx context) { }
[CommandCmif(0)]
// CreateContext(nn::ssl::sf::SslVersion, u64, pid) -> object<nn::ssl::sf::ISslContext>
// CreateContext(nn::ssl::sf::SslVersion, u64 pid_placeholder, pid) -> object<nn::ssl::sf::ISslContext>
public ResultCode CreateContext(ServiceCtx context)
{
SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32();
@@ -126,14 +126,18 @@ namespace Ryujinx.HLE.HOS.Services.Ssl
}
[CommandCmif(100)]
// CreateContextForSystem(u64 pid, nn::ssl::sf::SslVersion, u64)
// CreateContextForSystem(nn::ssl::sf::SslVersion, u64 pid_placeholder, pid) -> object<nn::ssl::sf::ISslContextForSystem>
public ResultCode CreateContextForSystem(ServiceCtx context)
{
ulong pid = context.RequestData.ReadUInt64();
SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32();
#pragma warning disable IDE0059 // Remove unnecessary value assignment
ulong pidPlaceholder = context.RequestData.ReadUInt64();
#pragma warning restore IDE0059
Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { pid, sslVersion, pidPlaceholder });
// Note: We use ISslContext here instead of ISslContextForSystem class because Ryujinx implements both in one class.
MakeObject(context, new ISslContext(context.Request.HandleDesc.PId, sslVersion));
Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { sslVersion });
return ResultCode.Success;
}

View File

@@ -20,5 +20,7 @@ namespace Ryujinx.HLE.HOS.SystemState
SimplifiedChinese,
TraditionalChinese,
BrazilianPortuguese,
Polish,
Thai,
}
}

View File

@@ -23,7 +23,9 @@ namespace Ryujinx.HLE.HOS.SystemState
"es-419",
"zh-Hans",
"zh-Hant",
"pt-BR"
"pt-BR",
"pl",
"th"
];
internal long DesiredKeyboardLayout { get; private set; }

View File

@@ -18,5 +18,7 @@ namespace Ryujinx.HLE.HOS.SystemState
TraditionalChinese,
SimplifiedChinese,
BrazilianPortuguese,
Polish,
Thai,
}
}

View File

@@ -1,12 +1,19 @@
using Ryujinx.Common.Memory;
using System;
using System.Buffers.Binary;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.Horizon.Sdk.Ns
{
public struct ApplicationControlProperty
{
public Array16<ApplicationTitle> Title;
/// <summary>
/// Use <see cref="Title"/> to access titles instead of accessing them directly.
/// </summary>
public Array16<ApplicationTitle> TitleBlock;
public Array37<byte> Isbn;
public StartupUserAccountValue StartupUserAccount;
public UserAccountSwitchLockValue UserAccountSwitchLock;
@@ -58,7 +65,10 @@ namespace Ryujinx.Horizon.Sdk.Ns
public RepairFlagValue RepairFlag;
public byte ProgramIndex;
public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag;
public Array4<byte> Reserved3214;
public byte ApplicationErrorCodePrefix;
public TitleCompressionValue TitleCompression;
public byte AcdIndex;
public byte ApparentPlatform;
public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration;
public ApplicationJitConfiguration JitConfiguration;
public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors;
@@ -74,6 +84,47 @@ namespace Ryujinx.Horizon.Sdk.Ns
public readonly string DisplayVersionString => Encoding.UTF8.GetString(DisplayVersion.AsSpan()).TrimEnd('\0');
public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0');
public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0');
private const int TitleCount = 32;
private const int TitleEntrySize = 0x300;
/// <summary>
/// Returns the resolved title entries. When <see cref="TitleCompression"/> is
/// <see cref="TitleCompressionValue.Enable"/>, the raw <see cref="TitleBlock"/> bytes are
/// decompressed (raw deflate) from 0x3000 into 0x6000 bytes yielding up to 32 entries.
/// Otherwise the 16 uncompressed entries from <see cref="TitleBlock"/> are returned directly.
/// </summary>
public readonly ApplicationTitle[] Title
{
get
{
var titles = new ApplicationTitle[TitleCount];
if (TitleCompression != TitleCompressionValue.Enable)
{
TitleBlock.AsSpan().CopyTo(titles);
return titles;
}
ReadOnlySpan<byte> titleBytes = MemoryMarshal.AsBytes(TitleBlock.AsSpan());
ushort compressedBlobSize = BinaryPrimitives.ReadUInt16LittleEndian(titleBytes);
ReadOnlySpan<byte> compressedBlob = titleBytes.Slice(2, compressedBlobSize);
byte[] decompressed = new byte[TitleCount * TitleEntrySize];
using (var compressedStream = new MemoryStream(compressedBlob.ToArray()))
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
{
deflateStream.ReadExactly(decompressed, 0, decompressed.Length);
}
MemoryMarshal.Cast<byte, ApplicationTitle>(decompressed).CopyTo(titles);
return titles;
}
}
public struct ApplicationTitle
{
@@ -130,6 +181,8 @@ namespace Ryujinx.Horizon.Sdk.Ns
TraditionalChinese = 13,
SimplifiedChinese = 14,
BrazilianPortuguese = 15,
Polish = 16,
Thai = 17,
}
public enum Organization
@@ -302,5 +355,11 @@ namespace Ryujinx.Horizon.Sdk.Ns
Deny = 0,
Allow = 1,
}
public enum TitleCompressionValue : byte
{
Disable = 0,
Enable = 1,
}
}
}

View File

@@ -42,6 +42,7 @@ namespace Ryujinx.Ava
public static bool PreviewerDetached { get; private set; }
public static bool UseHardwareAcceleration { get; private set; }
public static string BackendThreadingArg { get; private set; }
public static bool CoreDumpArg { get; private set; }
private const uint MbIconwarning = 0x30;
@@ -81,6 +82,8 @@ namespace Ryujinx.Ava
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
CoreDumpArg = coreDumpArg;
// 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,
// the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user.

View File

@@ -28,11 +28,6 @@
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-arm64'">
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>
<!--
FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json.
@@ -54,7 +49,7 @@
<PackageReference Include="Svg.Controls.Avalonia" />
<PackageReference Include="Svg.Controls.Skia.Avalonia" />
<PackageReference Include="DynamicData" />
<PackageReference Include="FluentAvaloniaUI.NoAnim" />
<PackageReference Include="FluentAvaloniaUI" />
<PackageReference Include="CommandLineParser" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="DiscordRichPresence" />

View File

@@ -1404,7 +1404,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
if (string.IsNullOrWhiteSpace(data.Name))
{
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
foreach (ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
{
if (!controlTitle.NameString.IsEmpty())
{
@@ -1417,7 +1417,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
if (string.IsNullOrWhiteSpace(data.Developer))
{
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
foreach (ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
{
if (!controlTitle.PublisherString.IsEmpty())
{

View File

@@ -24,6 +24,8 @@ namespace Ryujinx.Ava.Systems.Configuration.System
SimplifiedChinese,
TraditionalChinese,
BrazilianPortuguese,
Polish,
Thai,
}
public static class LanguageEnumHelper

View File

@@ -554,7 +554,7 @@ namespace Ryujinx.Ava.Systems
return false;
}
if (Program.Version.Contains("dirty") || !ReleaseInformation.IsValid)
if (Program.Version.Contains("dirty") || !ReleaseInformation.IsValid || RunningPlatform.IsIntelMac)
{
if (showWarnings)
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Windowing;
using Gommon;
using Ryujinx.Ava.Common.Locale;
@@ -53,6 +54,9 @@ namespace Ryujinx.Ava
{
Name = FormatTitle();
// Disable menu animations
FAUISettings.SetAnimationsEnabledAtAppLevel(false);
AvaloniaXamlLoader.Load(this);
if (OperatingSystem.IsMacOS())

View File

@@ -174,6 +174,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private string _screenshotKey = "F8";
private float _volume;
private ApplicationData _currentApplicationData;
private bool _pendingRestart;
private readonly AutoResetEvent _rendererWaitEvent;
private int _customVSyncInterval;
private int _customVSyncIntervalPercentageProxy;
@@ -370,6 +371,39 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool CanScanAmiiboBinaries => AmiiboBinReader.HasAmiiboKeyFile;
public bool IsSkylanderRequested
{
get => field && _isGameRunning;
set
{
field = value;
OnPropertyChanged();
}
}
public bool HasSkylander
{
get => field && _isGameRunning;
set
{
field = value;
OnPropertyChanged();
}
}
public bool ShowSkylanderActions
{
get => field && _isGameRunning;
set
{
field = value;
OnPropertyChanged();
}
}
public bool ShowLoadProgress
{
get;
@@ -1029,7 +1063,7 @@ namespace Ryujinx.Ava.UI.ViewModels
string dialogMessage =
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage);
if (ContentManager.AreKeysAlredyPresent(systemDirectory))
if (ContentManager.AreKeysAlreadyPresent(systemDirectory))
{
dialogMessage +=
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys
@@ -1217,6 +1251,14 @@ namespace Ryujinx.Ava.UI.ViewModels
await LoadApplication(_currentApplicationData);
}
else if (_pendingRestart)
{
_pendingRestart = false;
Logger.Info?.Print(LogClass.Application, $"Restarting emulation for '{_currentApplicationData.Name}'");
await LoadApplication(_currentApplicationData);
}
else
{
// Otherwise, clear state.
@@ -1225,6 +1267,21 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public void RestartEmulation()
{
if (AppHost is null || _currentApplicationData is null)
{
Logger.Warning?.Print(LogClass.Application, "RestartEmulation called but no application is running.");
return;
}
Logger.Info?.Print(LogClass.Application, $"Restart requested for '{_currentApplicationData.Name}'");
_pendingRestart = true;
AppHost.Stop();
}
private void Update_StatusBar(object sender, StatusUpdatedEventArgs args)
{
if (ShowMenuAndStatusBar && !ShowLoadProgress)
@@ -1864,6 +1921,46 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
}
public async Task OpenSkylanderWindow()
{
if (AppHost.Device.System.SearchingForSkylander(out int deviceId))
{
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(
new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
{
Patterns = ["*.sky", "*.bin", "*.dmp", "*.dump"],
},
},
});
if (result.HasValue)
{
// Open reading stream from the first file.
await using var stream = await result.Value.OpenReadAsync();
using var streamReader = new BinaryReader(stream);
// Reads all the content of file as a text.
byte[] data = new byte[1024];
var count = streamReader.Read(data, 0, 1024);
if (count < 1024)
{
return;
}
else
{
AppHost.Device.System.ScanSkylander(deviceId, data);
}
}
}
}
public async Task RemoveSkylander()
{
AppHost.Device.System.RemoveSkylander();
}
public void ReloadRenderDocApi()
{
@@ -2117,8 +2214,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; } =
Commands.CreateConditional<MainWindowViewModel>(vm => vm?.SelectedApplication != null,
viewModel => ShortcutHelper.CreateAppShortcut(

View File

@@ -28,9 +28,6 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty]
public partial ObservableCollection<ProfileImageModel> Images { get; set; }
[ObservableProperty]
public partial bool FirmwareFound { get; set; }
[ObservableProperty]
public partial Color BackgroundColor { get; set; } = Colors.White;
@@ -46,15 +43,17 @@ namespace Ryujinx.Ava.UI.ViewModels
};
}
private int _selectedIndex = -1;
public int SelectedIndex
{
get => _selectedIndex;
get;
set
{
_selectedIndex = value;
SelectedImage = value == -1 ? null : Images[value].Data;
field = value;
SelectedImage = field == -1
? null
: Images[field].Data;
OnPropertyChanged();
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 System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
namespace Ryujinx.Ava.UI.ViewModels
{
@@ -9,35 +9,20 @@ namespace Ryujinx.Ava.UI.ViewModels
{
public UserProfileViewModel()
{
Profiles = new ObservableCollection<BaseModel>();
LostProfiles = new ObservableCollection<UserProfile>();
LostProfiles.CollectionChanged += LostProfilesChanged;
Profiles = [];
LostProfiles = [];
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()
{
LostProfiles.CollectionChanged -= LostProfilesChanged;
}
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));
GC.SuppressFinalize(this);
}
}
}

View File

@@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.ViewModels
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)
{

View File

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

View File

@@ -47,18 +47,13 @@
</StackPanel>
<StackPanel Orientation="Horizontal" IsEnabled="{Binding ShowLedColorPicker}">
<TextBlock MinWidth="75" MaxWidth="200" Text="{ext:Locale ControllerSettingsLedColor}" />
<ui:ColorPickerButton
<ColorPicker
Margin="5"
IsMoreButtonVisible="False"
UseColorPalette="False"
UseColorTriangle="False"
UseColorWheel="False"
ShowAcceptDismissButtons="False"
IsAlphaEnabled="False"
AttachedToVisualTree="ColorPickerButton_OnAttachedToVisualTree"
ColorChanged="ColorPickerButton_OnColorChanged"
AttachedToVisualTree="ColorPicker_OnAttachedToVisualTree"
ColorChanged="ColorPicker_OnColorChanged"
Color="{Binding LedColor, Mode=TwoWay}">
</ui:ColorPickerButton>
</ColorPicker>
</StackPanel>
</StackPanel>
</UserControl>

View File

@@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
@@ -30,19 +31,17 @@ namespace Ryujinx.UI.Views.Input
InitializeComponent();
}
private void ColorPickerButton_OnColorChanged(ColorPickerButton sender, ColorButtonColorChangedEventArgs args)
private void ColorPicker_OnColorChanged(object sender, ColorChangedEventArgs args)
{
if (!args.NewColor.HasValue)
return;
if (!ViewModel.EnableLedChanging)
return;
if (ViewModel.TurnOffLed)
return;
ViewModel.ParentModel.SelectedGamepad.SetLed(args.NewColor.Value.ToUInt32());
ViewModel.ParentModel.SelectedGamepad.SetLed(args.NewColor.ToUInt32());
}
private void ColorPickerButton_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
private void ColorPicker_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (!ViewModel.EnableLedChanging)
return;

View File

@@ -136,7 +136,7 @@
<MenuItem
Command="{Binding ManageProfiles}"
Padding="0"
Header="{ext:Locale UserProfiles_MenuBarOptions_OpenUserProfiles}"
Header="{ext:Locale MenuBarOptionsManageUserProfiles}"
Icon="{ext:Icon fa-solid fa-user}"
IsEnabled="{Binding EnableNonGameRunningControls}"
Classes="withCheckbox">
@@ -167,6 +167,12 @@
Icon="{ext:Icon fa-solid fa-stop}"
InputGesture="Escape"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
Name="RestartEmulationMenuItem"
Header="{ext:Locale MenuBarOptionsRestartEmulation}"
Icon="{ext:Icon fa-solid fa-rotate-right}"
InputGesture="Ctrl + R"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{ext:Locale MenuBarOptionsSimulateWakeUpMessage}" Icon="{ext:Icon fa-solid fa-sun}" />
<Separator />
<MenuItem
@@ -184,6 +190,22 @@
IsVisible="{Binding CanScanAmiiboBinaries}"
InputGesture="Ctrl + B"
IsEnabled="{Binding IsAmiiboBinRequested}" />
<MenuItem
Command="{Binding OpenSkylanderWindow}"
AttachedToVisualTree="ScanSkylanderMenuItem_AttachedToVisualTree"
Header="{ext:Locale MenuBarActionsScanSkylander}"
Icon="{ext:Icon fa-solid fa-cube}"
IsVisible="{Binding ShowSkylanderActions}"
InputGesture="Ctrl + S"
IsEnabled="{Binding IsSkylanderRequested}" />
<MenuItem
Command="{Binding RemoveSkylander}"
AttachedToVisualTree="RemoveSkylanderMenuItem_AttachedToVisualTree"
Header="{ext:Locale MenuBarActionsRemoveSkylander}"
Icon="{ext:Icon fa-solid fa-cube}"
IsVisible="{Binding ShowSkylanderActions}"
InputGesture="Ctrl + D"
IsEnabled="{Binding HasSkylander}" />
<MenuItem
Command="{Binding TakeScreenshot}"
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"

View File

@@ -43,6 +43,7 @@ namespace Ryujinx.Ava.UI.Views.Main
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
StopEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
RestartEmulationMenuItem.Command = Commands.Create(() => ViewModel.RestartEmulation());
CheatManagerMenuItem.Command = Commands.CreateSilentFail(OpenCheatManagerForCurrentApp);
InstallFileTypesMenuItem.Command = Commands.Create(InstallFileTypes);
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
@@ -193,6 +194,20 @@ namespace Ryujinx.Ava.UI.Views.Main
ViewModel.IsAmiiboBinRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasAmiiboKeyFile;
}
private void ScanSkylanderMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is MenuItem)
ViewModel.IsSkylanderRequested = ViewModel.AppHost.Device.System.SearchingForSkylander(out _);
ViewModel.ShowSkylanderActions = string.Equals(ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(), "0100CCC0002E6000");
}
private void RemoveSkylanderMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is MenuItem)
ViewModel.HasSkylander = ViewModel.AppHost.Device.System.HasSkylander(out _);
ViewModel.ShowSkylanderActions = string.Equals(ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(), "0100CCC0002E6000");
}
private async Task InstallFileTypes()
{
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();

View File

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

View File

@@ -1,10 +1,6 @@
using Avalonia.Controls;
using Avalonia.Data;
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.Navigation;
using Ryujinx.Ava.Common.Locale;
@@ -12,10 +8,6 @@ using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
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;
namespace Ryujinx.Ava.UI.Views.User
@@ -23,75 +15,90 @@ namespace Ryujinx.Ava.UI.Views.User
public partial class UserEditorView : RyujinxControl<TempProfile>
{
private NavigationDialogHost _parent;
private ContentManager _contentManager;
private UserProfile _profile;
private TempProfile _tempProfile;
private bool _isNewUser;
public static uint MaxProfileNameLength => 0x20;
public bool IsDeletable => _profile.UserId != AccountManager.DefaultUserId;
public string UserEditorTitle => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UserProfiles_UserEditorTitle, _profile.Name);
public UserEditorView()
{
InitializeComponent();
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct);
AddHandler(Frame.NavigatedToEvent, (s, e) =>
{
NavigatedTo(e);
}, RoutingStrategies.Direct);
}
private void NavigatedTo(NavigationEventArgs arg)
{
if (!Program.PreviewerDetached)
return;
if (arg.NavigationMode == NavigationMode.New)
if (Program.PreviewerDetached)
{
(NavigationDialogHost parent, UserProfile profile, bool isNewUser) =
((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
switch (arg.NavigationMode)
{
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;
_profile = profile;
_isNewUser = isNewUser;
_parent = parent;
break;
}
DataValidationErrors.ClearErrors(NameBox);
DataValidationErrors.ClearErrors(ImageBox);
ImageBox.Bind(Border.BorderBrushProperty, new DynamicResourceExtension("AppListHoverBackgroundColor"));
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - " +
$"{(_isNewUser ? LocaleManager.Instance[LocaleKeys.UserEditorTitleCreate] : LocaleManager.Instance[LocaleKeys.UserEditorTitle])}";
ViewModel = new TempProfile(_profile);
_tempProfile = ViewModel;
_contentManager = _parent.ContentManager;
ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null;
AddPictureButton.IsVisible = _isNewUser;
ChangePictureButton.IsVisible = !_isNewUser;
IdLabel.IsVisible = _profile != null;
IdText.IsVisible = _profile != 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)
{
bool hasUnsavedChanges =
_isNewUser
? (ViewModel.Name != string.Empty || ViewModel.Image != null)
: (_profile.Name != ViewModel.Name || _profile.Image != ViewModel.Image);
if (hasUnsavedChanges)
if (_isNewUser)
{
bool confirm = await ContentDialogHelper.CreateChoiceDialog(
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesTitle],
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesMessage],
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesSubMessage]);
if (confirm)
if (ViewModel.Name != string.Empty || ViewModel.Image != null)
{
if (await ContentDialogHelper.CreateChoiceDialog(
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
{
_parent?.GoBack();
}
}
else
{
_parent?.GoBack();
}
}
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)
{
DataValidationErrors.ClearErrors(NameBox);
DataValidationErrors.ClearErrors(ImageBox);
ImageBox.Bind(Border.BorderBrushProperty, new DynamicResourceExtension("AppListHoverBackgroundColor"));
bool nameEmpty = 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)
if (string.IsNullOrWhiteSpace(ViewModel.Name))
{
DataValidationErrors.SetError(NameBox,new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfiles_EmptyNameError]));
DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfileEmptyNameError]));
return;
}
else if (imageMissing)
if (ViewModel.Image == null)
{
DataValidationErrors.SetError(ImageBox, new DataValidationException(""));
ImageBox.BorderBrush = Brush.Parse("#ff99a4");
_parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, ViewModel));
return;
}
@@ -133,7 +130,6 @@ namespace Ryujinx.Ava.UI.Views.User
_profile.Name = ViewModel.Name;
_profile.Image = ViewModel.Image;
_profile.UpdateState();
_parent.AccountManager.SetUserName(_profile.UserId, _profile.Name);
_parent.AccountManager.SetUserImage(_profile.UserId, _profile.Image);
}
@@ -149,80 +145,17 @@ namespace Ryujinx.Ava.UI.Views.User
_parent?.GoBack();
}
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
public void SelectProfileImage()
{
if (ViewModel.FirmwareFound)
{
_parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _tempProfile));
}
_parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, ViewModel));
}
private async void Import_OnClick(object sender, RoutedEventArgs e)
private void ChangePictureButton_Click(object sender, RoutedEventArgs e)
{
var window = (Window)this.GetVisualRoot()!;
var result = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
if (_profile != null || _isNewUser)
{
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_SupportedImageFormatDialogTitle],
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);
SelectProfileImage();
}
return streamJpg.ToArray();
}
}
}
}

View File

@@ -1,81 +1,73 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView"
xmlns="https://github.com/avaloniaui"
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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
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"
Width="528"
Focusable="True"
x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel">
d:DesignWidth="578"
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>
<viewModels:UserFirmwareAvatarSelectorViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto,Auto">
<Border
<Grid
Margin="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto,Auto">
<ListBox
Grid.Row="1"
Padding="2.5"
BorderThickness="1"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
CornerRadius="5"
BorderThickness="0"
SelectedIndex="{Binding SelectedIndex}"
Height="400"
ItemsSource="{Binding Images}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<ListBox
Grid.Row="1"
Background="Transparent"
SelectedIndex="{Binding SelectedIndex}"
Height="400"
ItemsSource="{Binding Images}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel
Orientation="Horizontal"
Margin="0"
HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Width" Value="85" />
<Setter Property="MaxWidth" Value="85" />
<Setter Property="MinWidth" Value="85" />
</Style>
<Style Selector="ListBoxItem /template/ Rectangle#SelectionIndicator">
<Setter Property="MinHeight" Value="70" />
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
<Setter Property="IsVisible" Value="True" />
</Style>
</ListBox.Styles>
<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>
VerticalAlignment="Center">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel
Orientation="Horizontal"
Margin="0"
HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Width" Value="85" />
<Setter Property="MaxWidth" Value="85" />
<Setter Property="MinWidth" Value="85" />
</Style>
<Style Selector="ListBoxItem /template/ Rectangle#SelectionIndicator">
<Setter Property="MinHeight" Value="70" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate>
<Panel
Background="{Binding BackgroundColor}"
Margin="5">
<Image Source="{Binding Data, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
</Panel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel
Grid.Row="3"
Orientation="Horizontal"
Spacing="10"
Margin="0,30,0,0"
Margin="0 24 0 0"
HorizontalAlignment="Left">
<Button
Width="50"
MinWidth="50"
Height="37"
Height="35"
Click="GoBack">
<ui:SymbolIcon Symbol="Back" />
</Button>
@@ -84,29 +76,23 @@
Grid.Row="3"
Orientation="Horizontal"
Spacing="10"
Margin="0,30,0,0"
Margin="0 24 0 0"
HorizontalAlignment="Right">
<ui:ColorPickerButton
FlyoutPlacement="Top"
IsMoreButtonVisible="False"
UseColorPalette="False"
UseColorTriangle="False"
UseColorWheel="False"
ShowAcceptDismissButtons="False"
<ColorPicker
IsAlphaEnabled="False"
Color="{Binding BackgroundColor, Mode=TwoWay}"
Name="ColorButton">
<ui:ColorPickerButton.Styles>
<ColorPicker.Styles>
<Style Selector="Grid#Root > DockPanel > Grid">
<Setter Property="IsVisible" Value="False" />
</Style>
</ui:ColorPickerButton.Styles>
</ui:ColorPickerButton>
</ColorPicker.Styles>
</ColorPicker>
<Button
Height="37"
Click="ChooseButton_OnClick">
<TextBlock Text="{ext:Locale UserProfiles_ButtonChooseAvatar}" />
</Button>
Content="{ext:Locale AvatarChoose}"
Height="35"
Name="ChooseButton"
Click="ChooseButton_OnClick" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,15 +1,12 @@
using Avalonia.Interactivity;
using Avalonia.Threading;
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.IO;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Views.User
{
@@ -17,53 +14,44 @@ namespace Ryujinx.Ava.UI.Views.User
{
private NavigationDialogHost _parent;
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)
{
ContentManager = contentManager;
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)
{
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(() =>
if (Program.PreviewerDetached)
{
bool found = ContentManager.GetCurrentFirmwareVersion() != null;
Dispatcher.UIThread.Post(() =>
if (arg.NavigationMode == NavigationMode.New)
{
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)
{
_parent.GoBack();
@@ -71,31 +59,32 @@ namespace Ryujinx.Ava.UI.Views.User
private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
{
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))
if (ViewModel.SelectedImage != null)
{
canvas.Clear(new SKColor(
ViewModel.BackgroundColor.R,
ViewModel.BackgroundColor.G,
ViewModel.BackgroundColor.B,
ViewModel.BackgroundColor.A));
canvas.DrawBitmap(bitmap, 0, 0);
}
using MemoryStream streamJpg = new();
using SKBitmap bitmap = SKBitmap.Decode(ViewModel.SelectedImage);
using SKBitmap newBitmap = new(bitmap.Width, bitmap.Height);
using (SKImage image = SKImage.FromBitmap(newBitmap))
using (SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100))
{
dataJpeg.SaveTo(streamJpg);
}
using (SKCanvas canvas = new(newBitmap))
{
canvas.Clear(new SKColor(
ViewModel.BackgroundColor.R,
ViewModel.BackgroundColor.G,
ViewModel.BackgroundColor.B,
ViewModel.BackgroundColor.A));
canvas.DrawBitmap(bitmap, 0, 0);
}
_profile.Image = streamJpg.ToArray();
_parent.GoBack();
using (SKImage image = SKImage.FromBitmap(newBitmap))
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
x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView"
xmlns="https://github.com/avaloniaui"
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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
d:DesignWidth="550"
d:DesignHeight="450"
mc:Ignorable="d"
Width="500"
Height="400"
Focusable="True"
x:DataType="viewModels:UserProfileViewModel">
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
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>
<viewModels:UserProfileViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto">
<Grid HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" RowDefinitions="*,Auto">
<Border
CornerRadius="5"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1"
Grid.Row="0"
Padding="2.5">
Grid.Row="0">
<Panel>
<ListBox
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
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>
<DataTemplate>
<Border
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="4">
CornerRadius="5">
<Grid Margin="0" ColumnDefinitions="*,Auto">
<TextBlock
HorizontalAlignment="Stretch"
Margin="5"
Text="{Binding UserId}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button Grid.Column="1"
HorizontalAlignment="Right"
Margin="5"
Command="{Binding Recover}"
CommandParameter="{Binding}">
<TextBlock Text="{ext:Locale UserProfiles_RecoverProfile}" />
</Button>
Click="Recover"
CommandParameter="{Binding}"
Content="{ext:Locale Recover}"/>
</Grid>
</Border>
</DataTemplate>
@@ -67,12 +56,12 @@
<TextBlock
IsVisible="{Binding IsEmpty}"
TextAlignment="Center"
Text="{ext:Locale UserProfiles_RecoverProfile_EmptyList}"/>
Text="{ext:Locale UserProfilesRecoverEmptyList}"/>
</Panel>
</Border>
<StackPanel
Grid.Row="1"
Margin="0,30,0,0"
Margin="0 24 0 0"
Orientation="Horizontal">
<Button
Width="50"

View File

@@ -27,12 +27,11 @@ namespace Ryujinx.Ava.UI.Views.User
switch (arg.NavigationMode)
{
case NavigationMode.New:
case NavigationMode.Back:
NavigationDialogHost parent = (NavigationDialogHost)arg.Parameter;
_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;
}
@@ -43,5 +42,10 @@ namespace Ryujinx.Ava.UI.Views.User
{
_parent?.GoBack();
}
private void Recover(object sender, RoutedEventArgs e)
{
_parent?.RecoverLostAccounts();
}
}
}

View File

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

View File

@@ -55,7 +55,7 @@ namespace Ryujinx.Ava.UI.Views.User
}
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);
}
@@ -127,8 +127,8 @@ namespace Ryujinx.Ava.UI.Views.User
{
if (button.DataContext is SaveModel saveModel)
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.UserProfiles_DeleteSaveNote],
LocaleManager.Instance[LocaleKeys.UserProfiles_IrreversibleActionNote],
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DeleteUserSave],
LocaleManager.Instance[LocaleKeys.IrreversibleActionNote],
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
string.Empty);

View File

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

View File

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

View File

@@ -41,6 +41,7 @@
<KeyBinding Gesture="Escape" Command="{Binding ExitCurrentState}" />
<KeyBinding Gesture="Ctrl+A" Command="{Binding OpenAmiiboWindow}" />
<KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" />
<KeyBinding Gesture="Ctrl+R" Command="{Binding RestartEmulation}" />
<KeyBinding Gesture="Ctrl+Shift+R" Command="{Binding ReloadRenderDocApi}" />
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding ToggleCapture}" />
</Window.KeyBindings>

View File

@@ -1,5 +1,7 @@
using Avalonia.Platform.Storage;
using Gommon;
using Ryujinx.Common.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -11,29 +13,42 @@ namespace Ryujinx.Ava.Utilities
extension(IStorageProvider storageProvider)
{
public Task<Optional<IStorageFolder>> OpenSingleFolderPickerAsync(FolderPickerOpenOptions openOptions = null) =>
storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, false))
CoreDumpable(() => storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, false)))
.Then(folders => folders.FindFirst());
public Task<Optional<IStorageFile>> OpenSingleFilePickerAsync(FilePickerOpenOptions openOptions = null) =>
storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, false))
CoreDumpable(() => storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, false)))
.Then(files => files.FindFirst());
public Task<Optional<IReadOnlyList<IStorageFolder>>> OpenMultiFolderPickerAsync(FolderPickerOpenOptions openOptions = null) =>
storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, true))
CoreDumpable(() => storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, true)))
.Then(folders => folders.Count > 0 ? Optional.Of(folders) : default);
public Task<Optional<IReadOnlyList<IStorageFile>>> OpenMultiFilePickerAsync(FilePickerOpenOptions openOptions = null) =>
storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, true))
CoreDumpable(() => storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, true)))
.Then(files => files.Count > 0 ? Optional.Of(files) : default);
}
private static async Task<T> CoreDumpable<T>(Func<Task<T>> picker)
{
OsUtils.SetCoreDumpable(true);
try
{
return await picker();
}
finally
{
if (!Program.CoreDumpArg)
OsUtils.SetCoreDumpable(false);
}
}
private static FilePickerOpenOptions FixOpenOptions(this FilePickerOpenOptions openOptions, bool allowMultiple)
{
if (openOptions is null)
return new FilePickerOpenOptions { AllowMultiple = allowMultiple };
openOptions.AllowMultiple = allowMultiple;
return openOptions;
}
@@ -43,7 +58,6 @@ namespace Ryujinx.Ava.Utilities
return new FolderPickerOpenOptions { AllowMultiple = allowMultiple };
openOptions.AllowMultiple = allowMultiple;
return openOptions;
}
}

View File

@@ -1,6 +1,5 @@
using Ryujinx.Common.Logging;
using System;
using System.Management;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
@@ -11,7 +10,7 @@ namespace Ryujinx.Ava.Utilities.SystemInfo
{
internal WindowsSystemInfo()
{
CpuName = $"{GetCpuidCpuName() ?? GetCpuNameWMI()} ; {LogicalCoreCount} logical"; // WMI is very slow
CpuName = $"{GetCpuidCpuName() ?? GetCpuNameFromRegistry()} ; {LogicalCoreCount} logical";
(RamTotal, RamAvailable) = GetMemoryStats();
}
@@ -28,25 +27,26 @@ namespace Ryujinx.Ava.Utilities.SystemInfo
return (0, 0);
}
private static string GetCpuNameWMI()
private static string GetCpuNameFromRegistry()
{
ManagementObjectCollection cpuObjs = GetWMIObjects("root\\CIMV2", "SELECT * FROM Win32_Processor");
if (cpuObjs != null)
try
{
foreach (ManagementBaseObject cpuObj in cpuObjs)
{
return cpuObj["Name"].ToString().Trim();
}
}
using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")?.Trim();
return key?.GetValue("ProcessorNameString")?.ToString()?.Trim();
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.Application, $"Registry CPU name lookup failed: {ex.Message}");
return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")?.Trim();
}
}
[StructLayout(LayoutKind.Sequential)]
private struct MemoryStatusEx
private struct MemoryStatusEx()
{
public uint Length;
public uint Length = (uint)Marshal.SizeOf<MemoryStatusEx>();
public uint MemoryLoad;
public ulong TotalPhys;
public ulong AvailPhys;
@@ -55,33 +55,10 @@ namespace Ryujinx.Ava.Utilities.SystemInfo
public ulong TotalVirtual;
public ulong AvailVirtual;
public ulong AvailExtendedVirtual;
public MemoryStatusEx()
{
Length = (uint)Marshal.SizeOf<MemoryStatusEx>();
}
}
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GlobalMemoryStatusEx(ref MemoryStatusEx lpBuffer);
private static ManagementObjectCollection GetWMIObjects(string scope, string query)
{
try
{
return new ManagementObjectSearcher(scope, query).Get();
}
catch (PlatformNotSupportedException ex)
{
Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
}
catch (COMException ex)
{
Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
}
return null;
}
}
}