mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-06-12 15:29:15 +00:00
Compare commits
56 Commits
Canary-1.3
...
d20dedda0e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d20dedda0e | ||
|
|
b44cb07235 | ||
|
|
4c16ef0572 | ||
|
|
a8ce2c845a | ||
|
|
583cac31eb | ||
|
|
ea3ee010f6 | ||
|
|
cfdfdb3e28 | ||
|
|
a710a8756f | ||
|
|
bb687b6523 | ||
|
|
7ec8844121 | ||
|
|
8b2c53fea2 | ||
|
|
6c0e9a0c41 | ||
|
|
2a7d476a85 | ||
|
|
189033b84c | ||
|
|
fd2c71462e | ||
|
|
efd5cdc706 | ||
|
|
7b10b13d0d | ||
|
|
a8cb33a7ed | ||
|
|
19b762e7b0 | ||
|
|
56a8892c12 | ||
|
|
32ee806070 | ||
|
|
4e81a4c2f4 | ||
|
|
9cae62096a | ||
|
|
648b609ebb | ||
|
|
5ae86fc493 | ||
|
|
6f90e47a73 | ||
|
|
ac5f9857e2 | ||
|
|
4b42087bd4 | ||
|
|
80cbf5d1fc | ||
|
|
cc6d2dc162 | ||
|
|
4ebc318da5 | ||
|
|
00dad0a5e2 | ||
|
|
b70e2e44cb | ||
|
|
012d1d6886 | ||
|
|
d1205dc95d | ||
|
|
6f95172bb6 | ||
|
|
8208d43d9e | ||
|
|
1260f93aaf | ||
|
|
1b3bf1473d | ||
|
|
081cdcab0c | ||
|
|
922775664c | ||
|
|
478b66fd49 | ||
|
|
a16a072155 | ||
|
|
a4a0fcd4da | ||
|
|
cc5b60bbca | ||
|
|
5ed94c365b | ||
|
|
fef93a453a | ||
|
|
82074eb191 | ||
|
|
bd388cf4f9 | ||
|
|
d271abe19a | ||
|
|
c154f66f26 | ||
|
|
f556e8b8fb | ||
|
|
99feaafbe6 | ||
|
|
fa55608587 | ||
|
|
4c64300576 | ||
|
|
0a3db19b28 |
@@ -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>
|
||||
|
||||
18
Ryujinx.sln
18
Ryujinx.sln
@@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.GAL", "src
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.OpenGL", "src\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj", "{9558FB96-075D-4219-8FFF-401979DC0B69}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.RenderDoc", "src\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj", "{D58FA894-27D5-4EAA-9042-AD422AD82931}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Texture", "src\Ryujinx.Graphics.Texture\Ryujinx.Graphics.Texture.csproj", "{E1B1AD28-289D-47B7-A106-326972240207}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Shader", "src\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj", "{03B955CD-AD84-4B93-AAA7-BF17923BBAA5}"
|
||||
@@ -45,6 +47,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Vic", "src
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Video", "src\Ryujinx.Graphics.Video\Ryujinx.Graphics.Video.csproj", "{FD4A2C14-8E3D-4957-ABBE-3C38897B3E2D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.Apple", "src\Ryujinx.Audio.Backends.Apple\Ryujinx.Audio.Backends.Apple.csproj", "{AC26EFF0-8593-4184-9A09-98E37EFFB32E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SDL3", "src\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj", "{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.OpenAL", "src\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj", "{0BE11899-DF2D-4BDE-B9EE-2489E8D35E7D}"
|
||||
@@ -555,6 +559,20 @@ Global
|
||||
{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x64.Build.0 = Release|Any CPU
|
||||
{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -5,8 +5,8 @@ Ryubing Locales uses a custom format, which uses a file for defining the support
|
||||
Each json file holds the locales for a specific part of the emulator, e.g. the Setup Wizard locales are in `SetupWizard.json`, and each locale entry in the file includes all the supported languages in the same place.
|
||||
|
||||
## Languages
|
||||
in the `/assets/` folder you will find the `Languages.json` file, which defines all the languages supported by the emulator.
|
||||
The file includes a table of the langauge codes and their langauge names.
|
||||
In the `/assets/` folder you will find the `Languages.json` file, which defines all the languages supported by the emulator.
|
||||
The file includes a table of the language codes and their language names.
|
||||
|
||||
#Example of the format for Languages.json
|
||||
{
|
||||
@@ -19,7 +19,7 @@ The file includes a table of the langauge codes and their langauge names.
|
||||
}
|
||||
|
||||
## Locales
|
||||
in the `/assets/Locales/` folder you will find the json files, which define all the locales supported by the emulator.
|
||||
In the `/assets/Locales/` folder you will find the json files, which define all the locales supported by the emulator.
|
||||
Each json file holds locales for a specific part of the emulator in a large array of locale objects.
|
||||
Each locale is made up an ID used for lookup and a list of the languages and their matching translations.
|
||||
Any empty string or null value will automatically use the English translation instead in the emulator.
|
||||
|
||||
329
assets/Locales/Dialog_Firmware.json
Normal file
329
assets/Locales/Dialog_Firmware.json
Normal file
@@ -0,0 +1,329 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "InstallFromFileDialogTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر ملف .XCI أو أرشيف .ZIP لتثبيت البرنامج الثابت منه",
|
||||
"de_DE": "Wählen Sie eine .XCI-Datei oder ein .ZIP-Archiv aus, um die Firmware zu installieren",
|
||||
"el_GR": "Επιλέξτε ένα αρχείο .XCI ή ένα αρχείο .ZIP για να εγκαταστήσετε το υλικολογισμικό",
|
||||
"en_US": "Choose an .XCI file or a .ZIP archive to install firmware from",
|
||||
"es_ES": "Elige un archivo .XCI o un archivo .ZIP para instalar el firmware",
|
||||
"fr_FR": "Choisissez un fichier .XCI ou une archive .ZIP pour installer le firmware",
|
||||
"he_IL": "בחר קובץ .XCI או ארכיון .ZIP להתקנת הקושחה ממנו",
|
||||
"it_IT": "Scegli un file .XCI o un archivio .ZIP per installare il firmware",
|
||||
"ja_JP": "ファームウェアをインストールするために .XCI ファイルまたは .ZIP アーカイブを選択",
|
||||
"ko_KR": "펌웨어를 설치할 .XCI 파일 또는 .ZIP 아카이브를 선택하세요",
|
||||
"no_NO": "Velg en .XCI-fil eller et .ZIP-arkiv for å installere firmware fra",
|
||||
"pl_PL": "Wybierz plik .XCI lub archiwum .ZIP, z którego chcesz zainstalować firmware",
|
||||
"pt_BR": "Escolha um arquivo .XCI ou um arquivo .ZIP para instalar o firmware",
|
||||
"ru_RU": "Выберите файл .XCI или архив .ZIP для установки прошивки",
|
||||
"sv_SE": "Välj en .XCI-fil eller ett .ZIP-arkiv för att installera firmware",
|
||||
"th_TH": "เลือกไฟล์ .XCI หรือไฟล์เก็บถาวร .ZIP เพื่อติดตั้งเฟิร์มแวร์จาก",
|
||||
"tr_TR": "Firmware yüklemek için bir .XCI dosyası veya .ZIP arşivi seçin",
|
||||
"uk_UA": "Виберіть файл .XCI або архів .ZIP для встановлення прошивки",
|
||||
"zh_CN": "选择一个 .XCI 文件或 .ZIP 存档来安装固件",
|
||||
"zh_TW": "選擇一個 .XCI 檔案或 .ZIP 封存檔來安裝韌體"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFromFolderDialogTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر مجلد لتثبيت الكوشحة منه",
|
||||
"de_DE": "Wählen Sie einen ORDNER aus, um die Firmware zu installieren",
|
||||
"el_GR": "Επιλέξτε έναν ΦΆΚΕΛΟ για να εγκαταστήσετε το firmware",
|
||||
"en_US": "Choose a FOLDER to install firmware from",
|
||||
"es_ES": "Elige una CARPETA para instalar el firmware",
|
||||
"fr_FR": "Choisissez un DOSSIER pour installer le firmware",
|
||||
"he_IL": "בחר תיקיה להתקנת הקושחה ממנה",
|
||||
"it_IT": "Scegli una CARTELLA per installare il firmware",
|
||||
"ja_JP": "ファームウェアをインストールするフォルダを選択",
|
||||
"ko_KR": "펌웨어를 설치할 폴더를 선택하세요",
|
||||
"no_NO": "Velg en MAPPE for å installere firmware fra",
|
||||
"pl_PL": "Wybierz FOLDER, z którego chcesz zainstalować firmware",
|
||||
"pt_BR": "Escolha uma PASTA para instalar o firmware",
|
||||
"ru_RU": "Выберите ПАПКУ для установки прошивки",
|
||||
"sv_SE": "Välj en MAPP för att installera firmware från",
|
||||
"th_TH": "เลือกโฟลเดอร์เพื่อติดตั้งเฟิร์มแวร์จากนั้น",
|
||||
"tr_TR": "Firmware yüklemek için bir KLASÖR seçin",
|
||||
"uk_UA": "Виберіть ПАПКУ для встановлення прошивки",
|
||||
"zh_CN": "选择一个文件夹来安装固件",
|
||||
"zh_TW": "選擇一個資料夾來安裝韌體"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerEmbeddedMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "هل ترغب في تثبيت البرنامج الثابت المدمج في هذه اللعبة؟ (البرنامج الثابت {0})",
|
||||
"de_DE": "Die in diesem Spiel enthaltene Firmware installieren? (Firmware {0})",
|
||||
"el_GR": "Θα θέλατε να εγκαταστήσετε το Firmware που είναι ενσωματωμένο σε αυτό το παιχνίδι; (Firmware {0})",
|
||||
"en_US": "Would you like to install the firmware embedded in this game? (Firmware {0})",
|
||||
"es_ES": "¿Quieres instalar el firmware incluido en este juego? (Firmware versión {0})",
|
||||
"fr_FR": "Voulez-vous installer le firmware intégré dans ce jeu ? (Firmware {0})",
|
||||
"he_IL": "האם תרצו להתקין את הקושחה המוטמעת במשחק הזה? (קושחה {0})",
|
||||
"it_IT": "Vuoi installare il firmware incluso in questo gioco? (Firmware {0})",
|
||||
"ja_JP": "このゲームに含まれるファームウェアをインストールしてよろしいですか? (ファームウェア {0})",
|
||||
"ko_KR": "이 게임에 포함된 펌웨어를 설치하시겠습니까?(Firmware {0})",
|
||||
"no_NO": "Ønsker du å installere fastvaren innebygd i dette spillet? (Firmware {0})",
|
||||
"pl_PL": "Czy chcesz zainstalować firmware wbudowany w tę grę? (Firmware {0})",
|
||||
"pt_BR": "Gostaria de instalar o firmware incluso neste jogo? (Firmware {0})",
|
||||
"ru_RU": "Хотите установить прошивку, встроенную в эту игру? (Прошивка {0})",
|
||||
"sv_SE": "Vill du installera det firmware som är inbäddat i detta spel? (Firmware {0})",
|
||||
"th_TH": "คุณต้องการติดตั้งเฟิร์มแวร์ที่ฝังอยู่ในเกมนี้หรือไม่? (เฟิร์มแวร์ {0})",
|
||||
"tr_TR": "Bu oyunun içine gömülü olan yazılımı yüklemek ister misiniz? (Firmware {0})",
|
||||
"uk_UA": "Бажаєте встановити прошивку, вбудовану в цю гру? (Прошивка {0})",
|
||||
"zh_CN": "要安装游戏文件中内嵌的系统固件吗?(固件版本 {0})",
|
||||
"zh_TW": "您想安裝遊戲內建的韌體嗎? (韌體 {0})"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerEmbeddedMessageSuccess",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتم العثور على أي برنامج ثابت مثبت ولكن ريوجينكس كان قادرا على تثبيت البرنامج الثابت {0} من اللعبة المقدمة.\nسيبدأ المحاكي الآن.",
|
||||
"de_DE": "Es wurde keine installierte Firmware gefunden, aber Ryujinx konnte die Firmware {0} aus dem bereitgestellten Spiel installieren.\nRyujinx wird nun gestartet.",
|
||||
"el_GR": "Δεν βρέθηκε εγκατεστημένο υλικολογισμικό, αλλά το Ryujinx κατάφερε να εγκαταστήσει το υλικολογισμικό {0} από το παρεχόμενο παιχνίδι.\nΟ προσομοιωτής θα ξεκινήσει τώρα.",
|
||||
"en_US": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\nThe emulator will now start.",
|
||||
"es_ES": "No se encontró ningún firmware instalado, pero Ryujinx pudo instalar el firmware {0} del juego proporcionado.\nEl emulador iniciará.",
|
||||
"fr_FR": "Aucun firmware installé n'a été trouvé mais Ryujinx a pu installer le firmware {0} à partir du jeu fourni.\nL'émulateur va maintenant démarrer.",
|
||||
"he_IL": "לא נמצאה קושחה מותקנת אבל ריוג'ינקס הצליח להתקין קושחה {0} מהמשחק שסופק. \nהאמולטור יופעל כעת.",
|
||||
"it_IT": "Non è stato trovato alcun firmware installato, ma Ryujinx è riuscito ad installare il firmware {0} dal gioco fornito.\nL'emulatore si avvierà adesso.",
|
||||
"ja_JP": "ファームウェアがインストールされていませんが, ゲームに含まれるファームウェア {0} をインストールできます.\nエミュレータが開始します。",
|
||||
"ko_KR": "설치된 펌웨어를 찾을 수 없지만 Ryujinx는 제공된 게임에서 펌웨어 {0}을(를) 설치할 수 있습니다.\n이제 에뮬레이터가 시작됩니다.",
|
||||
"no_NO": "Det ble ikke funnet noen installert fastvare, men Ryujinx kunne installere fastvare {0} fra det oppgitte spillet.\nemulatoren vil nå starte.",
|
||||
"pl_PL": "Nie znaleziono zainstalowanego oprogramowania, ale Ryujinx był w stanie zainstalować oprogramowanie {0} z dostarczonej gry.\n\nEmulator uruchomi się teraz.",
|
||||
"pt_BR": "Nenhum firmware instalado foi encontrado, mas o Ryujinx conseguiu instalar o firmware {0} a partir do jogo fornecido.\nO emulador será iniciado agora.",
|
||||
"ru_RU": "Установленной прошивки не было найдено, но Ryujinx удалось установить прошивку {0} из предоставленной игры.\nТеперь запустится эмулятор.",
|
||||
"sv_SE": "Inget installerat firmware hittades men Ryujinx kunde installera firmware {0} från angiven spel.\nEmulatorn kommer nu att startas.",
|
||||
"th_TH": "ไม่พบเฟิร์มแวร์ที่ติดตั้งไว้ แต่ Ryujinx จะติดตั้งเฟิร์มแวร์ได้ {0} จากเกมที่ให้มา\nขณะนี้โปรแกรมจำลองจะเริ่มทำงาน",
|
||||
"tr_TR": "Yüklü bir firmware bulunamadı, ancak Ryujinx sağlanan oyundan firmware {0} yüklemeyi başardı.\nEmülatör şimdi başlatılacak.",
|
||||
"uk_UA": "Встановлену прошивку не знайдено, але Ryujinx вдалося встановити прошивку {0} з наданої гри.\nТепер запуститься емулятор.",
|
||||
"zh_CN": "Ryujinx 模拟器已经从当前游戏文件中安装了系统固件 {0} 。\n模拟器现在可以正常运行了。",
|
||||
"zh_TW": "未找到已安裝的韌體,但 Ryujinx 可以從現有的遊戲安裝韌體{0}。\n模擬器現在可以執行。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerNotInstalledMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "لا يوجد برنامج ثابت مثبت",
|
||||
"de_DE": "Keine Firmware installiert.",
|
||||
"el_GR": "Δεν έχει εγκατασταθεί Firmware.",
|
||||
"en_US": "No Firmware Installed.",
|
||||
"es_ES": "No hay Firmware Instalado.",
|
||||
"fr_FR": "Aucun Firmware Installé.",
|
||||
"he_IL": "לא מותקנת קושחה.",
|
||||
"it_IT": "Nessun firmware installato.",
|
||||
"ja_JP": "ファームウェアがインストールされていません。",
|
||||
"ko_KR": "펌웨어가 설치되어 있지 .않음",
|
||||
"no_NO": "Ingen fastvare installert.",
|
||||
"pl_PL": "Brak Zainstalowanego Firmware'u.",
|
||||
"pt_BR": "Nenhum Firmware Instalado.",
|
||||
"ru_RU": "Прошивка не установлена.",
|
||||
"sv_SE": "Inget firmware installerat.",
|
||||
"th_TH": "ไม่มีการติดตั้งเฟิร์มแวร์",
|
||||
"tr_TR": "Yazılım Yüklü Değil.",
|
||||
"uk_UA": "Прошивка не встановлена.",
|
||||
"zh_CN": "未安装系统固件。",
|
||||
"zh_TW": "未安裝韌體。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerInstalledMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "تم تثبيت البرنامج الثابت {0}",
|
||||
"de_DE": "Firmware {0} wurde installiert",
|
||||
"el_GR": "Το Firmware {0} εγκαταστάθηκε",
|
||||
"en_US": "Firmware {0} was installed",
|
||||
"es_ES": "Se Instaló el Firmware {0}",
|
||||
"fr_FR": "Le firmware {0} a été installé",
|
||||
"he_IL": "הקושחה {0} הותקנה",
|
||||
"it_IT": "Il firmware {0} è stato installato",
|
||||
"ja_JP": "ファームウェア {0} がインストールされました",
|
||||
"ko_KR": "펌웨어 {0}이(가) 설치됨",
|
||||
"no_NO": "fastvare {0} ble installert",
|
||||
"pl_PL": "Firmware {0} został zainstalowany",
|
||||
"pt_BR": "Firmware {0} foi instalado",
|
||||
"ru_RU": "Прошивка {0} была установлена",
|
||||
"sv_SE": "Firmware {0} installerades",
|
||||
"th_TH": "เฟิร์มแวร์ {0} ติดตั้งแล้ว",
|
||||
"tr_TR": "Yazılım {0} yüklendi",
|
||||
"uk_UA": "Встановлено прошивку {0}",
|
||||
"zh_CN": "已安装系统固件 {0}",
|
||||
"zh_TW": "已安裝韌體{0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerFirmwareNotFound",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتم العثور على برنامج ثابت للنظام صالح في {0}.",
|
||||
"de_DE": "Es wurde keine gültige System-Firmware gefunden in {0}.",
|
||||
"el_GR": "Δεν βρέθηκε έγκυρο Firmware συστήματος στο {0}.",
|
||||
"en_US": "A valid system firmware was not found in {0}.",
|
||||
"es_ES": "No se pudo encontrar un firmware válido en {0}.",
|
||||
"fr_FR": "Un firmware valide n'a pas été trouvé dans {0}.",
|
||||
"he_IL": "לא נמצאה קושחת מערכת תקפה ב-{0}.",
|
||||
"it_IT": "Un firmware del sistema valido non è stato trovato in {0}.",
|
||||
"ja_JP": "{0} には有効なシステムファームウェアがありません。",
|
||||
"ko_KR": "{0}에서 유효한 시스템 펌웨어를 찾을 수 없습니다.",
|
||||
"no_NO": "En gyldig systemfastvare ble ikke funnet i {0}.",
|
||||
"pl_PL": "Nie znaleziono prawidłowego firmware'u systemowego w {0}.",
|
||||
"pt_BR": "Um firmware de sistema válido não foi encontrado em {0}.",
|
||||
"ru_RU": "Не удалось найти действительную системную прошивку в {0}.",
|
||||
"sv_SE": "Ett giltigt systemfirmware hittades inte i {0}.",
|
||||
"th_TH": "ไม่พบเฟิร์มแวร์ของระบบที่ถูกต้อง {0}.",
|
||||
"tr_TR": "{0} da geçerli bir sistem firmware'i bulunamadı.",
|
||||
"uk_UA": "Дійсна прошивка системи не знайдена в {0}.",
|
||||
"zh_CN": "在路径 {0} 中找不到有效的 Switch 系统固件。",
|
||||
"zh_TW": "在 {0} 中未發現有效的系統韌體。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت البرنامج الثابت {0}",
|
||||
"de_DE": "Installiere Firmware {0}",
|
||||
"el_GR": "Εγκατάσταση Firmware {0}",
|
||||
"en_US": "Install Firmware {0}",
|
||||
"es_ES": "Instalar Firmware {0}",
|
||||
"fr_FR": "Installer le Firmware {0}",
|
||||
"he_IL": "התקן קושחה {0}",
|
||||
"it_IT": "Installa firmware {0}",
|
||||
"ja_JP": "ファームウェア {0} をインストール",
|
||||
"ko_KR": "펌웨어 {0} 설치",
|
||||
"no_NO": "Installer fastvare {0}",
|
||||
"pl_PL": "Zainstaluj Firmware {0}",
|
||||
"pt_BR": "Instalar Firmware {0}",
|
||||
"ru_RU": "Установить прошивку {0}",
|
||||
"sv_SE": "Installera firmware {0}",
|
||||
"th_TH": "ติดตั้งเฟิร์มแวร์ {0}",
|
||||
"tr_TR": "Firmware {0} Yükle",
|
||||
"uk_UA": "Встановити прошивку {0}",
|
||||
"zh_CN": "安装系统固件 {0}",
|
||||
"zh_TW": "安裝韌體 {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerMainMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "سيتم تثبيت إصدار النظام {0}.",
|
||||
"de_DE": "Systemversion {0} wird jetzt installiert.",
|
||||
"el_GR": "Θα εγκατασταθεί η έκδοση συστήματος {0}.",
|
||||
"en_US": "System version {0} will be installed.",
|
||||
"es_ES": "Se instalará la versión de sistema {0}.",
|
||||
"fr_FR": "La version {0} du système sera installée.",
|
||||
"he_IL": "גירסת המערכת {0} תותקן.",
|
||||
"it_IT": "La versione del sistema {0} sarà installata.",
|
||||
"ja_JP": "システムバージョン {0} がインストールされます。",
|
||||
"ko_KR": "시스템 버전 {0}이(가) 설치됩니다.",
|
||||
"no_NO": "Systemversjon {0} vil bli installert.",
|
||||
"pl_PL": "Wersja systemu {0} zostanie zainstalowana.",
|
||||
"pt_BR": "A versão do sistema {0} será instalada.",
|
||||
"ru_RU": "Будет установлена версия прошивки {0}.",
|
||||
"sv_SE": "Systemversion {0} kommer att installeras.",
|
||||
"th_TH": "ระบบเวอร์ชั่น {0} ได้รับการติดตั้งเร็วๆ นี้",
|
||||
"tr_TR": "Sistem sürümü {0} yüklenecek.",
|
||||
"uk_UA": "Буде встановлено версію системи {0}.",
|
||||
"zh_CN": "即将安装系统固件版本 {0} 。",
|
||||
"zh_TW": "即將安裝系統韌體版本 {0}。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerSubMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "\n\nهذا سيحل محل إصدار النظام الحالي {0}.",
|
||||
"de_DE": "\n\nDies wird die aktuelle Systemversion {0} ersetzen.",
|
||||
"el_GR": "\n\nΑυτό θα αντικαταστήσει την τρέχουσα έκδοση συστήματος {0}.",
|
||||
"en_US": "\n\nThis will replace the current system version {0}.",
|
||||
"es_ES": "\n\nEsto reemplazará la versión de sistema actual, {0}.",
|
||||
"fr_FR": "\n\nCela remplacera la version actuelle du système {0}.",
|
||||
"he_IL": "\n\nזה יחליף את גרסת המערכת הנוכחית {0}.",
|
||||
"it_IT": "\n\nQuesta sostituirà l'attuale versione del sistema ({0}).",
|
||||
"ja_JP": "\n\n現在のシステムバージョン {0} を置き換えます。",
|
||||
"ko_KR": "\n\n현재 시스템 버전 {0}을(를) 대체합니다.",
|
||||
"no_NO": "\n\nDette erstatter den gjeldende systemversjonen {0}.",
|
||||
"pl_PL": "\n\nZastąpi to obecną wersję systemu {0}.",
|
||||
"pt_BR": "\n\nIsso substituirá a versão do sistema atual {0}.",
|
||||
"ru_RU": "\n\nЭто заменит текущую версию прошивки {0}.",
|
||||
"sv_SE": "\n\nDetta kommer att ersätta aktuella systemversionen {0}.",
|
||||
"th_TH": "\n\nสิ่งนี้จะแทนที่เวอร์ชั่นของระบบเวอร์ชั่นปัจจุบัน {0}.",
|
||||
"tr_TR": "\n\nBu şimdiki sistem sürümünün yerini alacak {0}.",
|
||||
"uk_UA": "\n\nЦе замінить поточну версію системи {0}.",
|
||||
"zh_CN": "\n\n替换当前系统固件版本 {0} 。",
|
||||
"zh_TW": "\n\n這將取代目前的系統韌體版本 {0}。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerConfirmMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "\nهل تريد المتابعة؟",
|
||||
"de_DE": "\n\nMöchtest du fortfahren?",
|
||||
"el_GR": "\n\nΘέλετε να συνεχίσετε;",
|
||||
"en_US": "\n\nDo you want to continue?",
|
||||
"es_ES": "\n\n¿Continuar?",
|
||||
"fr_FR": "\n\nVoulez-vous continuer ?",
|
||||
"he_IL": "\n\nהאם ברצונך להמשיך?",
|
||||
"it_IT": "\n\nVuoi continuare?",
|
||||
"ja_JP": "\n\n続けてよろしいですか?",
|
||||
"ko_KR": "\n\n계속하시겠습니까?",
|
||||
"no_NO": "\n\nVil du fortsette?",
|
||||
"pl_PL": "\n\nCzy chcesz kontynuować?",
|
||||
"pt_BR": "\n\nDeseja continuar?",
|
||||
"ru_RU": "\n\nПродолжить?",
|
||||
"sv_SE": "\n\nVill du fortsätta?",
|
||||
"th_TH": "\n\nคุณต้องการดำเนินการต่อหรือไม่?",
|
||||
"tr_TR": "\n\nDevam etmek istiyor musunuz?",
|
||||
"uk_UA": "\n\nВи хочете продовжити?",
|
||||
"zh_CN": "\n\n是否继续?",
|
||||
"zh_TW": "\n\n您確定要繼續嗎?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerWaitMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت البرنامج الثابت...",
|
||||
"de_DE": "Firmware wird installiert...",
|
||||
"el_GR": "Εγκατάσταση Firmware...",
|
||||
"en_US": "Installing Firmware...",
|
||||
"es_ES": "Instalando Firmware...",
|
||||
"fr_FR": "Installation du Firmware...",
|
||||
"he_IL": "מתקין קושחה...",
|
||||
"it_IT": "Installazione del firmware...",
|
||||
"ja_JP": "ファームウェアをインストール中...",
|
||||
"ko_KR": "펌웨어 설치 중...",
|
||||
"no_NO": "Installerer fastvare...",
|
||||
"pl_PL": "Instalowanie firmware'u...",
|
||||
"pt_BR": "Instalando firmware...",
|
||||
"ru_RU": "Установка прошивки...",
|
||||
"sv_SE": "Installerar firmware...",
|
||||
"th_TH": "กำลังติดตั้งเฟิร์มแวร์...",
|
||||
"tr_TR": "Firmware yükleniyor...",
|
||||
"uk_UA": "Встановлення прошивки...",
|
||||
"zh_CN": "安装系统固件中...",
|
||||
"zh_TW": "正在安裝韌體..."
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerSuccessMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "تم تثبيت إصدار النظام {0} بنجاح.",
|
||||
"de_DE": "Systemversion {0} wurde erfolgreich installiert.",
|
||||
"el_GR": "Η έκδοση συστήματος {0} εγκαταστάθηκε με επιτυχία.",
|
||||
"en_US": "System version {0} successfully installed.",
|
||||
"es_ES": "Versión de sistema {0} instalada con éxito.",
|
||||
"fr_FR": "Version du système {0} installée avec succès.",
|
||||
"he_IL": "גרסת המערכת {0} הותקנה בהצלחה.",
|
||||
"it_IT": "La versione del sistema {0} è stata installata.",
|
||||
"ja_JP": "システムバージョン {0} が正常にインストールされました。",
|
||||
"ko_KR": "시스템 버전 {0}이(가) 설치되었습니다.",
|
||||
"no_NO": "Systemversjon {0} ble installert.",
|
||||
"pl_PL": "Wersja systemu {0} została pomyślnie zainstalowana.",
|
||||
"pt_BR": "Versão do sistema {0} instalada com sucesso.",
|
||||
"ru_RU": "Прошивка версии {0} успешно установлена.",
|
||||
"sv_SE": "Systemversion {0} har installerats.",
|
||||
"th_TH": "ระบบเวอร์ชั่น {0} ติดตั้งเรียบร้อยแล้ว",
|
||||
"tr_TR": "Sistem sürümü {0} başarıyla yüklendi.",
|
||||
"uk_UA": "Версію системи {0} успішно встановлено.",
|
||||
"zh_CN": "成功安装系统固件版本 {0}。",
|
||||
"zh_TW": "成功安裝系統韌體版本 {0}。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
204
assets/Locales/Dialog_Keys.json
Normal file
204
assets/Locales/Dialog_Keys.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "InstallFromFileDialogTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر ملف .KEYS",
|
||||
"de_DE": "Wählen Sie eine .KEYS-Datei aus",
|
||||
"el_GR": "Επιλέξτε ένα αρχείο .KEYS",
|
||||
"en_US": "Choose a .KEYS file",
|
||||
"es_ES": "Elige un archivo .KEYS",
|
||||
"fr_FR": "Choisissez un fichier .KEYS",
|
||||
"he_IL": "בחר קובץ .KEYS",
|
||||
"it_IT": "Scegli un file .KEYS",
|
||||
"ja_JP": ".KEYS ファイルを選択",
|
||||
"ko_KR": ".KEYS 파일을 선택하세요",
|
||||
"no_NO": "Velg en .KEYS-fil",
|
||||
"pl_PL": "Wybierz plik .KEYS",
|
||||
"pt_BR": "Escolha um arquivo .KEYS",
|
||||
"ru_RU": "Выберите файл .KEYS",
|
||||
"sv_SE": "Välj en .KEYS-fil",
|
||||
"th_TH": "เลือกไฟล์ .KEYS",
|
||||
"tr_TR": ".KEYS dosyasını seçin",
|
||||
"uk_UA": "Виберіть файл .KEYS",
|
||||
"zh_CN": "选择一个 .KEYS 文件",
|
||||
"zh_TW": "選擇一個 .KEYS 檔案"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFromFolderDialogTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر مجلد لتثبيت المفاتيح منه",
|
||||
"de_DE": "Wählen Sie einen ORDNER aus, um die Schlüssel zu installieren",
|
||||
"el_GR": "Επιλέξτε έναν ΦΆΚΕΛΟ για να εγκαταστήσετε τα κλειδιά",
|
||||
"en_US": "Choose a FOLDER to install keys from",
|
||||
"es_ES": "Elige una CARPETA para instalar las claves",
|
||||
"fr_FR": "Choisissez un DOSSIER pour installer les clés",
|
||||
"he_IL": "בחר תיקיה להתקנת המפתחות ממנו",
|
||||
"it_IT": "Scegli una CARTELLA per installare le chiavi",
|
||||
"ja_JP": "フォルダを選択してキーをインストール",
|
||||
"ko_KR": "폴더를 선택하여 키를 설치하세요",
|
||||
"no_NO": "Velg en MAPPE for å installere nøklene fra",
|
||||
"pl_PL": "Wybierz FOLDER, aby zainstalować klucze",
|
||||
"pt_BR": "Escolha uma PASTA para instalar as chaves",
|
||||
"ru_RU": "Выберите ПАПКУ для установки ключей",
|
||||
"sv_SE": "Välj en MAPP för att installera nycklar från",
|
||||
"th_TH": "เลือกโฟลเดอร์เพื่อติดตั้งคีย์จาก",
|
||||
"tr_TR": "KLASÖR seçin ve anahtarları yükleyin",
|
||||
"uk_UA": "Виберіть ПАПКУ для встановлення ключів",
|
||||
"zh_CN": "选择一个文件夹来安装密钥",
|
||||
"zh_TW": "選擇一個資料夾來安裝密鑰"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerConfirmInstall",
|
||||
"Translations": {
|
||||
"ar_SA": "\nهل تريد المتابعة؟",
|
||||
"de_DE": "\n\nMöchtest du fortfahren?",
|
||||
"el_GR": "\n\nΘέλετε να συνεχίσετε;",
|
||||
"en_US": "\n\nDo you want to continue?",
|
||||
"es_ES": "\n\n¿Continuar?",
|
||||
"fr_FR": "\n\nVoulez-vous continuer ?",
|
||||
"he_IL": "\n\nהאם ברצונך להמשיך?",
|
||||
"it_IT": "\n\nVuoi continuare?",
|
||||
"ja_JP": "\n\n続けてよろしいですか?",
|
||||
"ko_KR": "\n\n계속하시겠습니까?",
|
||||
"no_NO": "\n\nVil du fortsette?",
|
||||
"pl_PL": "\n\nCzy chcesz kontynuować?",
|
||||
"pt_BR": "\n\nDeseja continuar?",
|
||||
"ru_RU": "\n\nПродолжить?",
|
||||
"sv_SE": "\n\nVill du fortsätta?",
|
||||
"th_TH": "\n\nคุณต้องการดำเนินการต่อหรือไม่?",
|
||||
"tr_TR": "\n\nDevam etmek istiyor musunuz?",
|
||||
"uk_UA": "\n\nВи хочете продовжити?",
|
||||
"zh_CN": "\n\n是否继续?",
|
||||
"zh_TW": "\n\n您確定要繼續嗎?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerKeysNotFound",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "An invalid Keys file was found in {0}.",
|
||||
"es_ES": "Se halló un archivo Keys inválido en {0}.",
|
||||
"fr_FR": "Un fichier de Clés invalide a été trouvé dans {0}.",
|
||||
"he_IL": "",
|
||||
"it_IT": "È stato trovato un file di chiavi non valido in {0}.",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "{0}에서 잘못된 키 파일이 발견.",
|
||||
"no_NO": "En ugyldig Keys-fil ble funnet i {0}.",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "Um arquivo Chaves inválido foi encontrado em {0}.",
|
||||
"ru_RU": "В {0} найден некорректный файл ключей.",
|
||||
"sv_SE": "En ogiltig nyckelfil hittades i {0}.",
|
||||
"th_TH": "พบไฟล์ Keys ที่ไม่ถูกต้องใน {0}.",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Виявлено неправильний файл ключів у теці {0}.",
|
||||
"zh_CN": "在 {0} 发现了一个无效的密匙文件。",
|
||||
"zh_TW": "找到無效的金鑰檔案 {0}。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerMainMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "سيتم تثبيت ملف مفاتيح جديد.",
|
||||
"de_DE": "Eine neue Schlüsseldatei wird installiert.",
|
||||
"el_GR": "Ένα νέο αρχείο Κλειδιών θα εγκατασταθεί.",
|
||||
"en_US": "New Keys file will be installed.",
|
||||
"es_ES": "Un nuevo archivo de Claves será instalado.",
|
||||
"fr_FR": "Nouveau fichier de Clés sera installé.",
|
||||
"he_IL": "קובץ מפתחות חדש יותקן.",
|
||||
"it_IT": "Un nuovo file di chiavi sarà installato.",
|
||||
"ja_JP": "新しいキー ファイルがインストールされます。",
|
||||
"ko_KR": "새로운 키 파일이 설치됩니다.",
|
||||
"no_NO": "Ny Keys-fil vil bli installert.",
|
||||
"pl_PL": "Nowy plik kluczy zostanie zainstalowany.",
|
||||
"pt_BR": "O novo arquivo Chaves será instalado.",
|
||||
"ru_RU": "Будут установлены новые ключи.",
|
||||
"sv_SE": "Ny nyckelfil kommer att installeras.",
|
||||
"th_TH": "กำลังติดตั้งไฟล์ Keys ใหม่",
|
||||
"tr_TR": "Yeni anahtar dosyası yüklenecek.",
|
||||
"uk_UA": "Новий файл Ключів буде встановлено.",
|
||||
"zh_CN": "将会安装新密匙文件。",
|
||||
"zh_TW": "將會安裝新增的金鑰檔案。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerSubMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "\n\nقد يحل هذا محل بعض المفاتيح المثبتة حاليًا.",
|
||||
"de_DE": "\n\nDies könnte einige der derzeit installierten Schlüssel ersetzen.",
|
||||
"el_GR": "\n\nΑυτό μπορεί να αντικαταστήσει μερικά από τα τρέχοντα εγκατεστημένα κλειδιά.",
|
||||
"en_US": "\n\nThis may replace some of the current installed Keys.",
|
||||
"es_ES": "\n\nEsto puede reemplazar algunas de las Keys actualmente instaladas.",
|
||||
"fr_FR": "\n\nCela peut remplacer certaines des Clés actuellement installées.",
|
||||
"he_IL": "\n\nזה עשוי להחליף חלק מהמפתחות המותקנים הנוכחיים.",
|
||||
"it_IT": "\n\nAlcune delle chiavi già installate potrebbero essere sovrascritte.",
|
||||
"ja_JP": "\n\nこれにより、現在インストールされているキーの一部が置き換えられる場合があります。",
|
||||
"ko_KR": "\n\n이로 인해 현재 설치된 키 중 일부가 대체될 수 있습니다.",
|
||||
"no_NO": "\n\nDette kan erstatte noen av de nåværende installerte nøklene.",
|
||||
"pl_PL": "\n\nTo może zastąpić niektóre z aktualnie zainstalowanych kluczy.",
|
||||
"pt_BR": "\n\nIsso pode substituir algumas das chaves instaladas atualmente.",
|
||||
"ru_RU": "\n\nЭто может заменить некоторые из текущих установленных ключей.",
|
||||
"sv_SE": "\n\nDetta kan ersätta några av de redan installerade nycklarna.",
|
||||
"th_TH": "\n\nสิ่งนี้อาจทำให้ไฟล์ Keys บางส่วนที่ติดตั้งอยู่ถูกแทนที่",
|
||||
"tr_TR": "\n\nBu, şu anda kurulu olan anahtarların bazılarının yerine geçebilir.",
|
||||
"uk_UA": "\n\nЦе замінить собою поточні файли Ключів.",
|
||||
"zh_CN": "\n\n这也许会替换掉一些当前已安装的密匙。",
|
||||
"zh_TW": "\n\n這將取代部分已安裝的金鑰。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerWaitMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "جارٍ تثبيت المفاتيح...",
|
||||
"de_DE": "Schlüssel werden installiert...",
|
||||
"el_GR": "Εγκατάσταση κλειδιών...",
|
||||
"en_US": "Installing Keys...",
|
||||
"es_ES": "Instalando Claves...",
|
||||
"fr_FR": "Installation des Clés...",
|
||||
"he_IL": "מתקין מפתחות...",
|
||||
"it_IT": "Installazione delle chiavi...",
|
||||
"ja_JP": "キーをインストールしています...",
|
||||
"ko_KR": "키 설치 중...",
|
||||
"no_NO": "Installere nøkler...",
|
||||
"pl_PL": "Instalowanie kluczy...",
|
||||
"pt_BR": "Instalando Chaves...",
|
||||
"ru_RU": "Установка ключей...",
|
||||
"sv_SE": "Installerar nycklar...",
|
||||
"th_TH": "กำลังดำเนินการติดตั้ง Keys...",
|
||||
"tr_TR": "Anahtarlar yükleniyor...",
|
||||
"uk_UA": "Встановлення Ключів...",
|
||||
"zh_CN": "安装密匙中。。。",
|
||||
"zh_TW": "正在安裝金鑰。。。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerSuccessMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "تم تثبيت ملف المفاتيح الجديد بنجاح.",
|
||||
"de_DE": "Neue Schlüsseldatei erfolgreich installiert.",
|
||||
"el_GR": "Το νέο αρχείο Κλειδιών εγκαταστάθηκε με επιτυχία.",
|
||||
"en_US": "New Keys file successfully installed.",
|
||||
"es_ES": "Nuevo archivo Keys instalado con éxito.",
|
||||
"fr_FR": "Nouveau fichier de Clés a été installé.",
|
||||
"he_IL": "הקובץ החדש של המפתחות הותקן בהצלחה.",
|
||||
"it_IT": "Nuovo file di chiavi installato con successo.",
|
||||
"ja_JP": "新しいキー ファイルが正常にインストールされました。",
|
||||
"ko_KR": "새로운 키 파일이 성공적으로 설치되었습니다.",
|
||||
"no_NO": "Ny Keys -fil installert.",
|
||||
"pl_PL": "Nowy plik kluczy został pomyślnie zainstalowany.",
|
||||
"pt_BR": "Novo arquivo de chaves instalado com sucesso.",
|
||||
"ru_RU": "Новые ключи успешно установлены.",
|
||||
"sv_SE": "Ny nyckelfil installerades.",
|
||||
"th_TH": "การติดตั้งไฟล์ Keys ใหม่เสร็จสมบูรณ์แล้ว",
|
||||
"tr_TR": "Yeni anahtar dosyası başarıyla yüklendi.",
|
||||
"uk_UA": "Нові ключі встановлено.",
|
||||
"zh_CN": "已成功安装新密匙文件。",
|
||||
"zh_TW": "成功安裝新增的金鑰檔案。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
154
assets/Locales/Error.json
Normal file
154
assets/Locales/Error.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "NoKeysFound",
|
||||
"Translations": {
|
||||
"ar_SA": "المفاتيح غير موجودة.",
|
||||
"de_DE": "Keys nicht gefunden.",
|
||||
"el_GR": "Τα κλειδιά δεν βρέθηκαν.",
|
||||
"en_US": "Keys not found.",
|
||||
"es_ES": "No se encontraron claves.",
|
||||
"fr_FR": "Clés non trouvées.",
|
||||
"he_IL": "המפתחות לא נמצאו.",
|
||||
"it_IT": "Chiavi non trovate.",
|
||||
"ja_JP": "Keys がありません。",
|
||||
"ko_KR": "키를 찾을 수 없음.",
|
||||
"no_NO": "Finner ikke nøkler.",
|
||||
"pl_PL": "Nie znaleziono kluczy.",
|
||||
"pt_BR": "Chaves não encontradas.",
|
||||
"ru_RU": "Ключи не найдены.",
|
||||
"sv_SE": "Nycklarna hittades inte.",
|
||||
"th_TH": "ไม่พบ คีย์",
|
||||
"tr_TR": "Keys bulunamadı.",
|
||||
"uk_UA": "Ключі не знайдено.",
|
||||
"zh_CN": "找不到密钥。",
|
||||
"zh_TW": "找不到金鑰。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "NoKeysFoundDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتمكن ريوجينكس من العثور على ملف \"prod.keys\" الخاص بك.",
|
||||
"de_DE": "Ryujinx konnte deine \"prod.keys\" Datei nicht finden.",
|
||||
"el_GR": "Το Ryujinx δεν κατάφερε να εντοπίσει το αρχείο \"prod.keys\".",
|
||||
"en_US": "Ryujinx was unable to find your \"prod.keys\" file.",
|
||||
"es_ES": "Ryujinx no pudo encontrar tus \"prod.keys\".",
|
||||
"fr_FR": "Ryujinx n'a pas pu trouver votre fichier \"prod.keys\".",
|
||||
"he_IL": "ריוג'ינקס לא הצליח למצוא את קובץ ה-\"prod.keys\" שלך.",
|
||||
"it_IT": "Ryujinx non è riuscito a trovare il file \"prod.keys\".",
|
||||
"ja_JP": "\"prod.keys\" が見つかりませんでした。",
|
||||
"ko_KR": "Ryujinx가 '\"prod.keys\" 파일을 찾지 못함.",
|
||||
"no_NO": "Ryujinx kunne ikke finne \"prod.keys\" filen din.",
|
||||
"pl_PL": "Ryujinx nie mógł znaleźć twojego pliku \"prod.keys\".",
|
||||
"pt_BR": "Ryujinx não conseguiu encontrar o seu arquivo '\"prod.keys\".",
|
||||
"ru_RU": "Ryujinx не удалось найти ваш \"prod.keys\" файл.",
|
||||
"sv_SE": "Ryujinx kunde inte hitta din \"prod.keys\"-fil.",
|
||||
"th_TH": "Ryujinx ไม่พบไฟล์ '\"prod.keys\" ในเครื่องของคุณ",
|
||||
"tr_TR": "Ryujinx \"prod.keys\" dosyasını bulamadı.",
|
||||
"uk_UA": "Ryujinx не вдалося знайти ваш файл \"prod.keys\".",
|
||||
"zh_CN": "Ryujinx 模拟器找不到“prod.keys”密钥文件。",
|
||||
"zh_TW": "Ryujinx 無法找到您的「prod.keys」檔案。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "NoFirmwareFound",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتم العثور على البرنامج الثابت",
|
||||
"de_DE": "Firmware nicht gefunden",
|
||||
"el_GR": "Το firmware δε βρέθηκε",
|
||||
"en_US": "Firmware not found",
|
||||
"es_ES": "No se encontró Firmware",
|
||||
"fr_FR": "Firmware introuvable",
|
||||
"he_IL": "קושחה לא נמצאה",
|
||||
"it_IT": "Firmware non trovato",
|
||||
"ja_JP": "ファームウェアがありません",
|
||||
"ko_KR": "펌웨어를 찾을 수 없음",
|
||||
"no_NO": "Fastvare ikke funnet",
|
||||
"pl_PL": "Nie znaleziono firmware'u",
|
||||
"pt_BR": "Firmware não encontrado",
|
||||
"ru_RU": "Прошивка не найдена",
|
||||
"sv_SE": "Firmware hittades inte",
|
||||
"th_TH": "ไม่พบ เฟิร์มแวร์",
|
||||
"tr_TR": "Firmware bulunamadı",
|
||||
"uk_UA": "Прошивка не знайдена",
|
||||
"zh_CN": "未安装系统固件",
|
||||
"zh_TW": "找不到韌體"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "NoFirmwareFoundDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتمكن ريوجينكس من العثور على أية برامج ثابتة مثبتة.",
|
||||
"de_DE": "Ryujinx konnte keine installierte Firmware finden!",
|
||||
"el_GR": "Το Ryujinx δεν κατάφερε να εντοπίσει κανένα εγκατεστημένο firmware.",
|
||||
"en_US": "Ryujinx was unable to find any firmwares installed.",
|
||||
"es_ES": "Ryujinx no pudo encontrar un firmware instalado.",
|
||||
"fr_FR": "Ryujinx n'a pas trouvé de firmware installé.",
|
||||
"he_IL": "ריוג'ינקס לא הצליחה למצוא קושחה מותקנת.",
|
||||
"it_IT": "Ryujinx non è riuscito a trovare alcun firmware installato.",
|
||||
"ja_JP": "インストールされたファームウェアが見つかりませんでした。",
|
||||
"ko_KR": "Ryujinx가 설치된 펌웨어를 찾을 수 없음.",
|
||||
"no_NO": "Ryujinx kunne ikke finne noen fastvare installert.",
|
||||
"pl_PL": "Ryujinx nie mógł znaleźć żadnego zainstalowanego firmware'u.",
|
||||
"pt_BR": "Ryujinx não conseguiu encontrar nenhum Firmware instalado.",
|
||||
"ru_RU": "Ryujinx не удалось найти ни одной установленной прошивки.",
|
||||
"sv_SE": "Ryujinx kunde inte hitta några installerade firmwares.",
|
||||
"th_TH": "Ryujinx ไม่พบ เฟิร์มแวร์ที่ติดตั้งไว้ในเครื่องของคุณ",
|
||||
"tr_TR": "Ryujinx yüklü herhangi firmware bulamadı.",
|
||||
"uk_UA": "Ryujinx не вдалося знайти жодної встановленої прошивки.",
|
||||
"zh_CN": "Ryujinx 模拟器未安装 Switch 系统固件。",
|
||||
"zh_TW": "Ryujinx 無法找到已安裝的任何韌體。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "FirmwareParsingFailed",
|
||||
"Translations": {
|
||||
"ar_SA": "خطأ في تحليل البرنامج الثابت",
|
||||
"de_DE": "Firmware-Analysierung-Fehler",
|
||||
"el_GR": "Σφάλμα ανάλυσης firmware",
|
||||
"en_US": "Firmware parsing error",
|
||||
"es_ES": "Error al analizar el Firmware",
|
||||
"fr_FR": "Erreur d'analyse du firmware",
|
||||
"he_IL": "שגיאת ניתוח קושחה",
|
||||
"it_IT": "Errore di analisi del firmware",
|
||||
"ja_JP": "ファームウェアのパーズエラー",
|
||||
"ko_KR": "펌웨어 구문 분석 오류",
|
||||
"no_NO": "Fastvare analysefeil",
|
||||
"pl_PL": "Błąd parsowania firmware'u",
|
||||
"pt_BR": "Erro de análise de firmware",
|
||||
"ru_RU": "Ошибка извлечения прошивки",
|
||||
"sv_SE": "Tolkningsfel i firmware",
|
||||
"th_TH": "เกิดข้อผิดพลาดในการวิเคราะห์เฟิร์มแวร์",
|
||||
"tr_TR": "Firmware çözümleme hatası",
|
||||
"uk_UA": "Помилка аналізу прошивки",
|
||||
"zh_CN": "固件文件解析出错",
|
||||
"zh_TW": "韌體解析錯誤"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "FirmwareParsingFailedDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتمكن ريوجينكس من تحليل البرامج الثابتة المتوفرة. يحدث هذا عادة بسبب المفاتيح القديمة.",
|
||||
"de_DE": "Ryujinx konnte die zu verfügung gestellte Firmware nicht analysieren. Ein möglicher Grund dafür sind veraltete keys.",
|
||||
"el_GR": "Το Ryujinx δεν κατάφερε να αναλύσει το συγκεκριμένο firmware. Αυτό συνήθως οφείλετε σε ξεπερασμένα/παλιά κλειδιά.",
|
||||
"en_US": "Ryujinx was unable to parse the provided firmware. This is usually caused by outdated keys.",
|
||||
"es_ES": "Ryujinx no pudo analizar el firmware. Normalmente esto ocurre debido a keys desfasadas.",
|
||||
"fr_FR": "Ryujinx n'a pas pu analyser le firmware fourni. Cela est généralement dû à des clés obsolètes.",
|
||||
"he_IL": "ריוג'ינקס לא הצליחה לנתח את הקושחה שסופקה. זה נגרם בדרך כלל על ידי מפתחות לא עדכניים.",
|
||||
"it_IT": "Ryujinx non è riuscito ad analizzare il firmware. Questo di solito è causato da chiavi non aggiornate.",
|
||||
"ja_JP": "ファームウェアをパーズできませんでした.通常,古いキーが原因です.",
|
||||
"ko_KR": "Ryujinx가 제공된 펌웨어를 구문 분석하지 못했습니다. 일반적으로 오래된 키로 인해 발생합니다.",
|
||||
"no_NO": "Ryujinx klarte ikke å analysere levert fastvare. Dette er vanligvis forårsaket av utdaterte nøkler.",
|
||||
"pl_PL": "Ryujinx nie był w stanie zparsować dostarczonego firmware'u. Jest to zwykle spowodowane nieaktualnymi kluczami.",
|
||||
"pt_BR": "Ryujinx não conseguiu ler o Firmware fornecido. Geralmente isso é causado por chaves desatualizadas.",
|
||||
"ru_RU": "Ryujinx не удалось распаковать выбранную прошивку. Обычно это вызвано устаревшими ключами.",
|
||||
"sv_SE": "Ryujinx kunde inte tolka angiven firmware. Detta sker oftast med utdaterade nycklar.",
|
||||
"th_TH": "Ryujinx ไม่สามารถวิเคราะห์เฟิร์มแวร์ที่ให้มาได้ ซึ่งมักมีสาเหตุมาจากคีย์ที่เก่าจนเกินไป",
|
||||
"tr_TR": "Ryujinx temin edilen firmware'i çözümleyemedi. Bu durum genellikle güncel olmayan keys'den kaynaklanır.",
|
||||
"uk_UA": "Ryujinx не вдалося проаналізувати прошивку. Зазвичай це спричинено застарілими ключами.",
|
||||
"zh_CN": "Ryujinx 模拟器无法解密当前固件,一般是由于使用了旧版的密钥导致的。",
|
||||
"zh_TW": "Ryujinx 無法解析所提供的韌體。這通常是由於金鑰過時造成的。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
579
assets/Locales/MenuBar_Actions.json
Normal file
579
assets/Locales/MenuBar_Actions.json
Normal file
@@ -0,0 +1,579 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "InstallKeysLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت المفاتيح",
|
||||
"de_DE": "Schlüssel installieren",
|
||||
"el_GR": "Εγκατάσταση Κλειδιών",
|
||||
"en_US": "Install Keys",
|
||||
"es_ES": "Instalar Claves",
|
||||
"fr_FR": "Installer des Clés",
|
||||
"he_IL": "התקנת מפתחות",
|
||||
"it_IT": "Installa chiavi",
|
||||
"ja_JP": "キーをインストール",
|
||||
"ko_KR": "설치 키",
|
||||
"no_NO": "Installere nøkler",
|
||||
"pl_PL": "Zainstaluj klucze",
|
||||
"pt_BR": "Instalar Chaves",
|
||||
"ru_RU": "Установить ключи",
|
||||
"sv_SE": "Installera nycklar",
|
||||
"th_TH": "ติดตั้ง Keys",
|
||||
"tr_TR": "Anahtarları Yükle",
|
||||
"uk_UA": "Встановити Ключі",
|
||||
"zh_CN": "安装密匙",
|
||||
"zh_TW": "安裝金鑰"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallKeysFromFileButton",
|
||||
"Translations": {
|
||||
"ar_SA": null,
|
||||
"de_DE": null,
|
||||
"el_GR": null,
|
||||
"en_US": ".KEYS",
|
||||
"es_ES": null,
|
||||
"fr_FR": null,
|
||||
"he_IL": null,
|
||||
"it_IT": null,
|
||||
"ja_JP": null,
|
||||
"ko_KR": null,
|
||||
"no_NO": null,
|
||||
"pl_PL": null,
|
||||
"pt_BR": null,
|
||||
"ru_RU": null,
|
||||
"sv_SE": null,
|
||||
"th_TH": null,
|
||||
"tr_TR": null,
|
||||
"uk_UA": null,
|
||||
"zh_CN": null,
|
||||
"zh_TW": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallKeysFromFolderButton",
|
||||
"Translations": {
|
||||
"ar_SA": "مجلد",
|
||||
"de_DE": "Verzeichnis",
|
||||
"el_GR": "Φάκελος",
|
||||
"en_US": "Folder",
|
||||
"es_ES": "Carpeta",
|
||||
"fr_FR": "Dossier",
|
||||
"he_IL": "תיקיה",
|
||||
"it_IT": "Cartella",
|
||||
"ja_JP": "フォルダ",
|
||||
"ko_KR": "폴더",
|
||||
"no_NO": "Mappe",
|
||||
"pl_PL": "Folder",
|
||||
"pt_BR": "Diretório",
|
||||
"ru_RU": "Папка",
|
||||
"sv_SE": "Katalog",
|
||||
"th_TH": "ไดเรกทอรี",
|
||||
"tr_TR": "Klasör",
|
||||
"uk_UA": "Тека",
|
||||
"zh_CN": "文件夹",
|
||||
"zh_TW": "資料夾"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFirmwareLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت البرنامج الثابت",
|
||||
"de_DE": "Firmware installieren",
|
||||
"el_GR": "Εγκατάσταση Firmware",
|
||||
"en_US": "Install Firmware",
|
||||
"es_ES": "Instalar Firmware",
|
||||
"fr_FR": "Installer le Firmware",
|
||||
"he_IL": "התקן קושחה",
|
||||
"it_IT": "Installa firmware",
|
||||
"ja_JP": "ファームウェアをインストール",
|
||||
"ko_KR": "펌웨어 설치",
|
||||
"no_NO": "Installer fastvare",
|
||||
"pl_PL": "Zainstaluj oprogramowanie",
|
||||
"pt_BR": "Instalar Firmware",
|
||||
"ru_RU": "Установить прошивку",
|
||||
"sv_SE": "Installera firmware",
|
||||
"th_TH": "ติดตั้งเฟิร์มแวร์",
|
||||
"tr_TR": "Yazılım Yükle",
|
||||
"uk_UA": "Встановити прошивку",
|
||||
"zh_CN": "安装系统固件",
|
||||
"zh_TW": "安裝韌體"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFirmwareFromFileButton",
|
||||
"Translations": {
|
||||
"ar_SA": ".XCI أو .ZIP",
|
||||
"de_DE": ".XCI oder .ZIP",
|
||||
"el_GR": ".XCI ή .ZIP",
|
||||
"en_US": ".XCI or .ZIP",
|
||||
"es_ES": ".XCI o .ZIP",
|
||||
"fr_FR": ".XCI ou .ZIP",
|
||||
"he_IL": ".XCI או .ZIP",
|
||||
"it_IT": ".XCI o .ZIP",
|
||||
"ja_JP": ".XCI または .ZIP",
|
||||
"ko_KR": ".XCI 또는 .ZIP",
|
||||
"no_NO": ".XCI eller .ZIP",
|
||||
"pl_PL": ".XCI lub .ZIP",
|
||||
"pt_BR": ".XCI ou .ZIP",
|
||||
"ru_RU": ".XCI или .ZIP",
|
||||
"sv_SE": ".XCI eller .ZIP",
|
||||
"th_TH": ".XCI หรือ .ZIP",
|
||||
"tr_TR": ".XCI veya .ZIP",
|
||||
"uk_UA": ".XCI або .ZIP",
|
||||
"zh_CN": ".XCI 或 .ZIP",
|
||||
"zh_TW": ".XCI 或 .ZIP"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFirmwareFromFolderButton",
|
||||
"Translations": {
|
||||
"ar_SA": "مجلد",
|
||||
"de_DE": "Verzeichnis",
|
||||
"el_GR": "Φάκελος",
|
||||
"en_US": "Folder",
|
||||
"es_ES": "Carpeta",
|
||||
"fr_FR": "Dossier",
|
||||
"he_IL": "תיקייה",
|
||||
"it_IT": "Cartella",
|
||||
"ja_JP": "フォルダー",
|
||||
"ko_KR": "폴더",
|
||||
"no_NO": "Mappe",
|
||||
"pl_PL": "Katalog",
|
||||
"pt_BR": "Diretório",
|
||||
"ru_RU": "Папка",
|
||||
"sv_SE": "Katalog",
|
||||
"th_TH": "โฟลเดอร์",
|
||||
"tr_TR": "Klasör",
|
||||
"uk_UA": "Тека",
|
||||
"zh_CN": "文件夹",
|
||||
"zh_TW": "資料夾"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ToolsLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Tools",
|
||||
"es_ES": "Herramientas",
|
||||
"fr_FR": "Outils",
|
||||
"he_IL": "",
|
||||
"it_IT": "Strumenti",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "도구",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "Ferramentas",
|
||||
"ru_RU": "Инструменты",
|
||||
"sv_SE": "Verktyg",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "工具",
|
||||
"zh_TW": "工具"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "MiiEditorButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Mii Editor",
|
||||
"es_ES": "Editor de Mii",
|
||||
"fr_FR": "Éditeur de Mii",
|
||||
"he_IL": "",
|
||||
"it_IT": "Editor di Mii",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "Mii 편집기",
|
||||
"no_NO": "Mii-redigerer",
|
||||
"pl_PL": "Edytor Mii",
|
||||
"pt_BR": "Editor de Mii",
|
||||
"ru_RU": "Редактор Mii",
|
||||
"sv_SE": "Mii-redigerare",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Редактор Mii",
|
||||
"zh_CN": "Mii 编辑器",
|
||||
"zh_TW": "Mii 編輯器"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "XCITrimmerButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "XCI-Dateien trimmen",
|
||||
"el_GR": "",
|
||||
"en_US": "Trim XCI Files",
|
||||
"es_ES": "Recortar Archivos XCI",
|
||||
"fr_FR": "Réduire les Fichiers XCI",
|
||||
"he_IL": "",
|
||||
"it_IT": "Riduci dimensioni dei file XCI",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "XCI 파일 트리머",
|
||||
"no_NO": "Trim XCI-filer",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "Reduzir Arquivos XCI",
|
||||
"ru_RU": "Обрезать XCI файлы",
|
||||
"sv_SE": "Optimera XCI-filer",
|
||||
"th_TH": "ตัดแต่งไฟล์ XCI",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Обрізати XCI файли",
|
||||
"zh_CN": "瘦身 XCI 文件",
|
||||
"zh_TW": "修剪 XCI 檔案"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "PauseEmulationButton",
|
||||
"Translations": {
|
||||
"ar_SA": "إيقاف التشغيل مؤقتًا",
|
||||
"de_DE": "Emulation pausieren",
|
||||
"el_GR": "Παύση προσομοίωσης",
|
||||
"en_US": "Pause Emulation",
|
||||
"es_ES": "Pausar Emulación",
|
||||
"fr_FR": "Pauser l'Émulation",
|
||||
"he_IL": "השהיית האמולציה",
|
||||
"it_IT": "Pausa emulazione",
|
||||
"ja_JP": "エミュレーション一時停止",
|
||||
"ko_KR": "에뮬레이션 일시중지",
|
||||
"no_NO": "Pause Emulatoren",
|
||||
"pl_PL": "Wstrzymaj emulację",
|
||||
"pt_BR": "Pausar emulação",
|
||||
"ru_RU": "Пауза эмуляции",
|
||||
"sv_SE": "Pausa emuleringen",
|
||||
"th_TH": "พักการจำลอง",
|
||||
"tr_TR": "Emülasyonu Duraklat",
|
||||
"uk_UA": "Пауза емуляції",
|
||||
"zh_CN": "暂停模拟",
|
||||
"zh_TW": "暫停模擬"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ResumeEmulationButton",
|
||||
"Translations": {
|
||||
"ar_SA": "استئناف المحاكاة",
|
||||
"de_DE": "Emulation fortsetzen",
|
||||
"el_GR": "Συνέχιση προσομοίωσης",
|
||||
"en_US": "Resume Emulation",
|
||||
"es_ES": "Reanudar Emulación",
|
||||
"fr_FR": "Reprendre l'Émulation",
|
||||
"he_IL": "המשך האמולציה",
|
||||
"it_IT": "Riprendi l'emulazione",
|
||||
"ja_JP": "エミュレーション再開",
|
||||
"ko_KR": "에뮬레이션 다시 시작",
|
||||
"no_NO": "Gjenoppta emuleringen",
|
||||
"pl_PL": "Wznów emulację",
|
||||
"pt_BR": "Retomar emulação",
|
||||
"ru_RU": "Продолжить эмуляцию",
|
||||
"sv_SE": "Återuppta emuleringen",
|
||||
"th_TH": "ดำเนินการจำลองต่อ",
|
||||
"tr_TR": "Emülasyonu Sürdür",
|
||||
"uk_UA": "Продовжити емуляцію",
|
||||
"zh_CN": "继续模拟",
|
||||
"zh_TW": "繼續模擬"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "StopEmulationButton",
|
||||
"Translations": {
|
||||
"ar_SA": "إيقاف المحاكاة",
|
||||
"de_DE": "Emulation beenden",
|
||||
"el_GR": "Διακοπή Εξομοίωσης",
|
||||
"en_US": "Stop Emulation",
|
||||
"es_ES": "Detener Emulación",
|
||||
"fr_FR": "Arrêter l'Émulation",
|
||||
"he_IL": "עצור אמולציה",
|
||||
"it_IT": "Arresta l'emulazione",
|
||||
"ja_JP": "エミュレーションを中止",
|
||||
"ko_KR": "에뮬레이션 중지",
|
||||
"no_NO": "Stopp Emulering",
|
||||
"pl_PL": "Zatrzymaj emulację",
|
||||
"pt_BR": "Parar a Emulação",
|
||||
"ru_RU": "Остановить эмуляцию",
|
||||
"sv_SE": "Stoppa emulering",
|
||||
"th_TH": "หยุดการจำลอง",
|
||||
"tr_TR": "Emülasyonu Durdur",
|
||||
"uk_UA": "Зупинити емуляцію",
|
||||
"zh_CN": "停止模拟",
|
||||
"zh_TW": "停止模擬"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RestartEmulationButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Restart Emulation",
|
||||
"es_ES": "",
|
||||
"fr_FR": "Redémarrer l'Émulation",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "Перезапустить эмуляцию",
|
||||
"sv_SE": "Starta om emulering",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ScanAmiiboButton",
|
||||
"Translations": {
|
||||
"ar_SA": "مسح Amiibo",
|
||||
"de_DE": "Amiibo scannen",
|
||||
"el_GR": "Σάρωση Amiibo",
|
||||
"en_US": "Scan Amiibo",
|
||||
"es_ES": "Escanear Amiibo",
|
||||
"fr_FR": "Scanner un Amiibo",
|
||||
"he_IL": "סרוק אמיבו",
|
||||
"it_IT": "Scansiona un Amiibo",
|
||||
"ja_JP": "Amiibo をスキャン",
|
||||
"ko_KR": "Amiibo 스캔",
|
||||
"no_NO": "Skann en Amiibo",
|
||||
"pl_PL": "Skanuj Amiibo",
|
||||
"pt_BR": "Escanear um Amiibo",
|
||||
"ru_RU": "Сканировать Amiibo",
|
||||
"sv_SE": "Skanna en Amiibo",
|
||||
"th_TH": "สแกนหา Amiibo",
|
||||
"tr_TR": "Bir Amiibo Tara",
|
||||
"uk_UA": "Сканувати Amiibo",
|
||||
"zh_CN": "扫描 Amiibo",
|
||||
"zh_TW": "掃描 Amiibo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ScanAmiiboFromBinButton",
|
||||
"Translations": {
|
||||
"ar_SA": "مسح Amiibo (.BIN)",
|
||||
"de_DE": "Amiibo scannen (.BIN)",
|
||||
"el_GR": "Σάρωση Amiibo (.BIN)",
|
||||
"en_US": "Scan Amiibo (.BIN)",
|
||||
"es_ES": "Escanear un Amiibo (.BIN)",
|
||||
"fr_FR": "Scanner un Amiibo (.BIN)",
|
||||
"he_IL": "סרוק Amiibo (.BIN)",
|
||||
"it_IT": "Scansiona un Amiibo (.BIN)",
|
||||
"ja_JP": "Amiibo をスキャン (.BIN)",
|
||||
"ko_KR": "Amiibo 스캔 (.BIN)",
|
||||
"no_NO": "Skann en Amiibo (.BIN)",
|
||||
"pl_PL": "Skanuj Amiibo (.BIN)",
|
||||
"pt_BR": "Escaneie um Amiibo (.BIN)",
|
||||
"ru_RU": "Сканировать Amiibo (.BIN)",
|
||||
"sv_SE": "Skanna en Amiibo (.BIN)",
|
||||
"th_TH": "สแกนอามีโบ (.BIN)",
|
||||
"tr_TR": "Amiibo Tara (.BIN)",
|
||||
"uk_UA": "Сканувати Amiibo (.BIN)",
|
||||
"zh_CN": "扫描 Amiibo (.BIN)",
|
||||
"zh_TW": "掃瞄 Amiibo (.BIN)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ScanSkylanderButton",
|
||||
"Translations": {
|
||||
"ar_SA": "فحص Skylander",
|
||||
"de_DE": "Skylander scannen",
|
||||
"el_GR": "Σάρωση Skylander",
|
||||
"en_US": "Scan Skylander",
|
||||
"es_ES": "Escanear Skylander",
|
||||
"fr_FR": "Scanner un Skylander",
|
||||
"he_IL": "סרוק אמיבו",
|
||||
"it_IT": "Scansiona un Skylander",
|
||||
"ja_JP": "Skylander をスキャン",
|
||||
"ko_KR": "Skylander 스캔",
|
||||
"no_NO": "Skann en Skylander",
|
||||
"pl_PL": "Skanuj Skylander",
|
||||
"pt_BR": "Escanear um Skylander",
|
||||
"ru_RU": "Сканировать Skylander",
|
||||
"sv_SE": "Skanna en Skylander",
|
||||
"th_TH": "สแกนหา Skylander",
|
||||
"tr_TR": "Bir Skylander Tara",
|
||||
"uk_UA": "Сканувати Skylander",
|
||||
"zh_CN": "扫描 Skylander",
|
||||
"zh_TW": "掃描 Skylander"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RemoveSkylanderButton",
|
||||
"Translations": {
|
||||
"ar_SA": "إزالة Skylander",
|
||||
"de_DE": "Skylander entfernen",
|
||||
"el_GR": "Αφαίρεση Skylander",
|
||||
"en_US": "Remove Skylander",
|
||||
"es_ES": "Eliminar Skylander",
|
||||
"fr_FR": "Supprimer un Skylander",
|
||||
"he_IL": "הסר Skylander",
|
||||
"it_IT": "Rimuovi Skylander",
|
||||
"ja_JP": "Skylander を削除",
|
||||
"ko_KR": "Skylander 제거",
|
||||
"no_NO": "Fjern Skylander",
|
||||
"pl_PL": "Usuń Skylander",
|
||||
"pt_BR": "Remover um Skylander",
|
||||
"ru_RU": "Удалить Skylander",
|
||||
"sv_SE": "Ta bort Skylander",
|
||||
"th_TH": "ลบ Skylander",
|
||||
"tr_TR": "Skylander'ı Kaldır",
|
||||
"uk_UA": "Видалити Skylander",
|
||||
"zh_CN": "移除 Skylander",
|
||||
"zh_TW": "移除 Skylander"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "TakeScreenshotButton",
|
||||
"Translations": {
|
||||
"ar_SA": "أخذ لقطة للشاشة",
|
||||
"de_DE": "Screenshot aufnehmen",
|
||||
"el_GR": "Λήψη Στιγμιότυπου",
|
||||
"en_US": "Take Screenshot",
|
||||
"es_ES": "Captura de Pantalla",
|
||||
"fr_FR": "Prendre une Capture d'Écran",
|
||||
"he_IL": "צלם מסך",
|
||||
"it_IT": "Cattura uno screenshot",
|
||||
"ja_JP": "スクリーンショットを撮影",
|
||||
"ko_KR": "스크린샷 찍기",
|
||||
"no_NO": "Ta skjermbilde",
|
||||
"pl_PL": "Zrób zrzut ekranu",
|
||||
"pt_BR": "Tirar Captura de tela",
|
||||
"ru_RU": "Сделать снимок экрана",
|
||||
"sv_SE": "Ta skärmbild",
|
||||
"th_TH": "ถ่ายภาพหน้าจอ",
|
||||
"tr_TR": "Ekran Görüntüsü Al",
|
||||
"uk_UA": "Зробити знімок екрана",
|
||||
"zh_CN": "保存截屏",
|
||||
"zh_TW": "儲存擷取畫面"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "HideUiButton",
|
||||
"Translations": {
|
||||
"ar_SA": "إخفاء واجهة المستخدم",
|
||||
"de_DE": "Oberfläche ausblenden",
|
||||
"el_GR": "Απόκρυψη UI",
|
||||
"en_US": "Hide UI",
|
||||
"es_ES": "Ocultar Interfaz",
|
||||
"fr_FR": "Masquer l'Interface",
|
||||
"he_IL": "הסתר ממשק משתמש ",
|
||||
"it_IT": "Nascondi l'interfaccia",
|
||||
"ja_JP": "UIを隠す",
|
||||
"ko_KR": "UI 숨기기",
|
||||
"no_NO": "Skjul brukergrensesnitt",
|
||||
"pl_PL": "Ukryj interfejs użytkownika",
|
||||
"pt_BR": "Esconder Interface",
|
||||
"ru_RU": "Скрыть интерфейс",
|
||||
"sv_SE": "Dölj gränssnittet",
|
||||
"th_TH": "ซ่อน UI",
|
||||
"tr_TR": "Arayüzü Gizle",
|
||||
"uk_UA": "Сховати інтерфейс",
|
||||
"zh_CN": "隐藏菜单栏和状态栏",
|
||||
"zh_TW": "隱藏 UI"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "StartRenderDocCaptureButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Start RenderDoc Frame Capture",
|
||||
"es_ES": "Iniciar una captura de fotograma de RenderDoc",
|
||||
"fr_FR": "Démarrer une capture de trame RenderDoc",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "RenderDoc 프레임 캡처 시작",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "启动 RenderDoc 帧捕获",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "EndRenderDocCaptureButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "End RenderDoc Frame Capture",
|
||||
"es_ES": "Detener la captura de fotograma de RenderDoc",
|
||||
"fr_FR": "Arrêter la capture de trame RenderDoc",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "RenderDoc 프레임 캡처 종료",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "结束 RenderDoc 帧捕获",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DiscardRenderDocCaptureButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Discard RenderDoc Frame Capture",
|
||||
"es_ES": "Descartar la captura de fotograma de RenderDoc",
|
||||
"fr_FR": "Supprimer la capture de trame RenderDoc",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "RenderDoc 프레임 캡처 폐기",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "丢弃 RenderDoc 帧捕获",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DiscardRenderDocCaptureToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.",
|
||||
"es_ES": "Finaliza la captura de fotograma de RenderDoc actualmente activa y descarta inmediatamente su resultado.",
|
||||
"fr_FR": "Met fin à la capture de trame RenderDoc en cours, en supprimant immédiatement son résultat.",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "현재 활성화된 RenderDoc 프레임 캡처를 종료하고 결과를 즉시 폐기합니다.",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。",
|
||||
"zh_TW": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
79
assets/Locales/MenuBar_File.json
Normal file
79
assets/Locales/MenuBar_File.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "ManageFileTypes",
|
||||
"Translations": {
|
||||
"ar_SA": "إدارة أنواع الملفات",
|
||||
"de_DE": "Dateitypen verwalten",
|
||||
"el_GR": "Διαχείριση τύπων αρχείων",
|
||||
"en_US": "Manage File Types",
|
||||
"es_ES": "Administrar Tipos de Archivo",
|
||||
"fr_FR": "Gérer les Types de Fichiers",
|
||||
"he_IL": "ניהול סוגי קבצים",
|
||||
"it_IT": "Gestisci i tipi di file",
|
||||
"ja_JP": "ファイル形式を管理",
|
||||
"ko_KR": "파일 형식 관리",
|
||||
"no_NO": "Behandle filtyper",
|
||||
"pl_PL": "Zarządzaj rodzajami plików",
|
||||
"pt_BR": "Gerenciar Tipos de Arquivos",
|
||||
"ru_RU": "Управление типами файлов",
|
||||
"sv_SE": "Hantera filtyper",
|
||||
"th_TH": "จัดการประเภทไฟล์",
|
||||
"tr_TR": "Dosya uzantılarını yönet",
|
||||
"uk_UA": "Керувати типами файлів",
|
||||
"zh_CN": "管理文件扩展名",
|
||||
"zh_TW": "管理檔案類型"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFileTypes",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت أنواع الملفات",
|
||||
"de_DE": "Dateitypen installieren",
|
||||
"el_GR": "Εγκαταστήσετε τύπους αρχείων.",
|
||||
"en_US": "Install File Types",
|
||||
"es_ES": "Instalar Tipos de Archivo",
|
||||
"fr_FR": "Installer des Types de Fichiers",
|
||||
"he_IL": "סוגי קבצי התקנה",
|
||||
"it_IT": "Installa i tipi di file",
|
||||
"ja_JP": "ファイル形式をインストール",
|
||||
"ko_KR": "파일 형식 설치",
|
||||
"no_NO": "Installer filtyper",
|
||||
"pl_PL": "Typy plików instalacyjnych",
|
||||
"pt_BR": "Instalar tipos de arquivos",
|
||||
"ru_RU": "Установить типы файлов",
|
||||
"sv_SE": "Installera filtyper",
|
||||
"th_TH": "ติดตั้งประเภทไฟล์",
|
||||
"tr_TR": "Dosya uzantılarını yükle",
|
||||
"uk_UA": "Встановити типи файлів",
|
||||
"zh_CN": "关联文件扩展名",
|
||||
"zh_TW": "安裝檔案類型"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "UninstallFileTypes",
|
||||
"Translations": {
|
||||
"ar_SA": "إزالة أنواع الملفات",
|
||||
"de_DE": "Dateitypen deinstallieren",
|
||||
"el_GR": "Απεγκαταστήσετε τύπους αρχείων",
|
||||
"en_US": "Uninstall File Types",
|
||||
"es_ES": "Desinstalar Tipos de Archivo",
|
||||
"fr_FR": "Désinstaller des Types de Fichiers",
|
||||
"he_IL": "סוגי קבצי הסרה",
|
||||
"it_IT": "Disinstalla i tipi di file",
|
||||
"ja_JP": "ファイル形式をアンインストール",
|
||||
"ko_KR": "파일 형식 제거",
|
||||
"no_NO": "Avinstaller filtyper",
|
||||
"pl_PL": "Typy plików dezinstalacyjnych",
|
||||
"pt_BR": "Desinstalar tipos de arquivos",
|
||||
"ru_RU": "Удалить типы файлов",
|
||||
"sv_SE": "Avinstallera filtyper",
|
||||
"th_TH": "ถอนการติดตั้งประเภทไฟล์",
|
||||
"tr_TR": "Dosya uzantılarını kaldır",
|
||||
"uk_UA": "Видалити типи файлів",
|
||||
"zh_CN": "取消关联扩展名",
|
||||
"zh_TW": "移除檔案類型"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
29
assets/Locales/StatusBar.json
Normal file
29
assets/Locales/StatusBar.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "FirmwareVersion",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Firmware Version: {0}",
|
||||
"es_ES": "Versión del Firmware: {0}",
|
||||
"fr_FR": "Version du Firmware : {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Versione firmware: {0}",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "펌웨어 버전 : {0}",
|
||||
"no_NO": "Fastvareversjon: {0}",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "Versão do Firmware: {0}",
|
||||
"ru_RU": "Версия прошивки: {0}",
|
||||
"sv_SE": "Firmware-version: {0}",
|
||||
"th_TH": "เวอร์ชันเฟิร์มแวร์: {0}",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Версія прошивки: {0}",
|
||||
"zh_CN": "系统固件版本:{0}",
|
||||
"zh_TW": "系統韌體版本: {0}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2050,7 +2050,9 @@
|
||||
010003C00B868000,"Ninjin: Clash of Carrots",online-broken,playable,2024-07-10 05:12:26
|
||||
0100746010E4C000,"NinNinDays",,playable,2022-11-20 15:17:29
|
||||
0100C9A00ECE6000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07
|
||||
010057D00ECE4000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07
|
||||
0100e0601c632000,"Nintendo 64™ – Nintendo Switch Online: MATURE 17+",,ingame,2025-02-03 22:27:00
|
||||
010037A0170D2000,"NINTENDO 64™ – Nintendo Switch Online 18+",,ingame,2025-02-03 22:27:00
|
||||
0100D870045B6000,"Nintendo Entertainment System™ - Nintendo Switch Online",online,playable,2022-07-01 15:45:06
|
||||
0100C4B0034B2000,"Nintendo Labo Toy-Con 01 Variety Kit",gpu,ingame,2022-08-07 12:56:07
|
||||
01001E9003502000,"Nintendo Labo Toy-Con 03 Vehicle Kit",services;crash,menus,2022-08-03 17:20:11
|
||||
@@ -2638,6 +2640,7 @@
|
||||
0100B16009C10000,"SINNER: Sacrifice for Redemption",nvdec;UE4;vulkan-backend-bug,playable,2022-08-12 20:37:33
|
||||
0100E9201410E000,"Sir Lovelot",,playable,2021-04-05 16:21:46
|
||||
0100134011E32000,"Skate City",,playable,2022-11-04 11:37:39
|
||||
0100a8501b66e000,"Skateboard Drifting with Maxwell Cat: The Game Simulator",,playable,2026-02-17 19:05:00
|
||||
0100B2F008BD8000,"Skee-Ball",,playable,2020-11-16 04:44:07
|
||||
01001A900F862000,"Skelattack",,playable,2021-06-09 15:26:26
|
||||
01008E700F952000,"Skelittle: A Giant Party!",,playable,2021-06-09 19:08:34
|
||||
@@ -3307,6 +3310,7 @@
|
||||
0100AFA011068000,"Voxel Pirates",,playable,2022-09-28 22:55:02
|
||||
0100BFB00D1F4000,"Voxel Sword",,playable,2022-08-30 14:57:27
|
||||
01004E90028A2000,"Vroom in the night sky",Needs Update;vulkan-backend-bug,playable,2023-02-20 02:32:29
|
||||
0100BFC01D976000,"Virtual Boy – Nintendo Classics",services,nothing,2026-02-17 11:26:59
|
||||
0100C7C00AE6C000,"VSR: Void Space Racing",,playable,2021-01-27 14:08:59
|
||||
0100B130119D0000,"Waifu Uncovered",crash,ingame,2023-02-27 01:17:46
|
||||
0100E29010A4A000,"Wanba Warriors",,playable,2020-10-04 17:56:22
|
||||
|
||||
|
@@ -168,7 +168,7 @@ namespace ARMeilleure.Common
|
||||
{
|
||||
_allocated.Dispose();
|
||||
|
||||
foreach (IntPtr page in _pages.Values)
|
||||
foreach (nint page in _pages.Values)
|
||||
{
|
||||
NativeAllocator.Instance.Free((void*)page);
|
||||
}
|
||||
|
||||
16
src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs
Normal file
16
src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Ryujinx.Audio.Backends.Apple
|
||||
{
|
||||
class AppleAudioBuffer
|
||||
{
|
||||
public readonly ulong DriverIdentifier;
|
||||
public readonly ulong SampleCount;
|
||||
public ulong SamplePlayed;
|
||||
|
||||
public AppleAudioBuffer(ulong driverIdentifier, ulong sampleCount)
|
||||
{
|
||||
DriverIdentifier = driverIdentifier;
|
||||
SampleCount = sampleCount;
|
||||
SamplePlayed = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
196
src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs
Normal file
196
src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Audio.Integration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Runtime.Versioning;
|
||||
using Ryujinx.Audio.Backends.Apple.Native;
|
||||
using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox;
|
||||
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.Apple
|
||||
{
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("ios")]
|
||||
public sealed class AppleHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
{
|
||||
private readonly ManualResetEvent _updateRequiredEvent;
|
||||
private readonly ManualResetEvent _pauseEvent;
|
||||
private readonly ConcurrentDictionary<AppleHardwareDeviceSession, byte> _sessions;
|
||||
private readonly bool _supportSurroundConfiguration;
|
||||
|
||||
public float Volume { get; set; }
|
||||
|
||||
public AppleHardwareDeviceDriver()
|
||||
{
|
||||
_updateRequiredEvent = new ManualResetEvent(false);
|
||||
_pauseEvent = new ManualResetEvent(true);
|
||||
_sessions = new ConcurrentDictionary<AppleHardwareDeviceSession, byte>();
|
||||
|
||||
_supportSurroundConfiguration = TestSurroundSupport();
|
||||
|
||||
Volume = 1f;
|
||||
}
|
||||
|
||||
private bool TestSurroundSupport()
|
||||
{
|
||||
try
|
||||
{
|
||||
AudioStreamBasicDescription format =
|
||||
GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6);
|
||||
|
||||
int result = AudioQueueNewOutput(
|
||||
ref format,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
0,
|
||||
out nint testQueue);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
AudioChannelLayout layout = new AudioChannelLayout
|
||||
{
|
||||
AudioChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_A,
|
||||
AudioChannelBitmap = 0,
|
||||
NumberChannelDescriptions = 0
|
||||
};
|
||||
|
||||
int layoutResult = AudioQueueSetProperty(
|
||||
testQueue,
|
||||
kAudioQueueProperty_ChannelLayout,
|
||||
ref layout,
|
||||
(uint)Marshal.SizeOf<AudioChannelLayout>());
|
||||
|
||||
if (layoutResult == 0)
|
||||
{
|
||||
AudioQueueDispose(testQueue, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
AudioQueueDispose(testQueue, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsSupported => OperatingSystem.IsMacOSVersionAtLeast(10, 5);
|
||||
|
||||
public ManualResetEvent GetUpdateRequiredEvent()
|
||||
=> _updateRequiredEvent;
|
||||
|
||||
public ManualResetEvent GetPauseEvent()
|
||||
=> _pauseEvent;
|
||||
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager,
|
||||
SampleFormat sampleFormat, uint sampleRate, uint channelCount)
|
||||
{
|
||||
if (channelCount == 0)
|
||||
{
|
||||
channelCount = 2;
|
||||
}
|
||||
|
||||
if (sampleRate == 0)
|
||||
{
|
||||
sampleRate = Constants.TargetSampleRate;
|
||||
}
|
||||
|
||||
if (direction != Direction.Output)
|
||||
{
|
||||
throw new NotImplementedException("Input direction is currently not implemented on Apple backend!");
|
||||
}
|
||||
|
||||
AppleHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
|
||||
|
||||
_sessions.TryAdd(session, 0);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
internal bool Unregister(AppleHardwareDeviceSession session)
|
||||
=> _sessions.TryRemove(session, out _);
|
||||
|
||||
internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate,
|
||||
uint channelCount)
|
||||
{
|
||||
uint formatFlags;
|
||||
uint bitsPerChannel;
|
||||
|
||||
switch (sampleFormat)
|
||||
{
|
||||
case SampleFormat.PcmInt8:
|
||||
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
bitsPerChannel = 8;
|
||||
break;
|
||||
case SampleFormat.PcmInt16:
|
||||
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
bitsPerChannel = 16;
|
||||
break;
|
||||
case SampleFormat.PcmInt32:
|
||||
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
bitsPerChannel = 32;
|
||||
break;
|
||||
case SampleFormat.PcmFloat:
|
||||
formatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
|
||||
bitsPerChannel = 32;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported sample format {sampleFormat}");
|
||||
}
|
||||
|
||||
uint bytesPerFrame = (bitsPerChannel / 8) * channelCount;
|
||||
|
||||
return new AudioStreamBasicDescription
|
||||
{
|
||||
SampleRate = sampleRate,
|
||||
FormatID = kAudioFormatLinearPCM,
|
||||
FormatFlags = formatFlags,
|
||||
BytesPerPacket = bytesPerFrame,
|
||||
FramesPerPacket = 1,
|
||||
BytesPerFrame = bytesPerFrame,
|
||||
ChannelsPerFrame = channelCount,
|
||||
BitsPerChannel = bitsPerChannel,
|
||||
Reserved = 0
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (AppleHardwareDeviceSession session in _sessions.Keys)
|
||||
{
|
||||
session.Dispose();
|
||||
}
|
||||
|
||||
_pauseEvent.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsDirection(Direction direction)
|
||||
=> direction != Direction.Input;
|
||||
|
||||
public bool SupportsSampleRate(uint sampleRate) => true;
|
||||
|
||||
public bool SupportsSampleFormat(SampleFormat sampleFormat)
|
||||
=> sampleFormat != SampleFormat.PcmInt24;
|
||||
|
||||
public bool SupportsChannelCount(uint channelCount)
|
||||
=> channelCount != 6 || _supportSurroundConfiguration;
|
||||
}
|
||||
}
|
||||
285
src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs
Normal file
285
src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using Ryujinx.Audio.Backends.Common;
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Runtime.Versioning;
|
||||
using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.Apple
|
||||
{
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("ios")]
|
||||
class AppleHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
{
|
||||
private const int NumBuffers = 3;
|
||||
|
||||
private readonly AppleHardwareDeviceDriver _driver;
|
||||
private readonly ConcurrentQueue<AppleAudioBuffer> _queuedBuffers = new();
|
||||
private readonly DynamicRingBuffer _ringBuffer = new();
|
||||
private readonly ManualResetEvent _updateRequiredEvent;
|
||||
|
||||
private readonly AudioQueueOutputCallback _callbackDelegate;
|
||||
private readonly GCHandle _gcHandle;
|
||||
|
||||
private nint _audioQueue;
|
||||
private readonly nint[] _audioQueueBuffers = new nint[NumBuffers];
|
||||
private readonly int[] _bufferBytesFilled = new int[NumBuffers];
|
||||
|
||||
private readonly int _bytesPerFrame;
|
||||
|
||||
private ulong _playedSampleCount;
|
||||
private bool _started;
|
||||
private float _volume = 1f;
|
||||
|
||||
private readonly object _lock = new();
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AudioQueueOutputCallback(
|
||||
nint userData,
|
||||
nint audioQueue,
|
||||
nint buffer);
|
||||
|
||||
public AppleHardwareDeviceSession(
|
||||
AppleHardwareDeviceDriver driver,
|
||||
IVirtualMemoryManager memoryManager,
|
||||
SampleFormat requestedSampleFormat,
|
||||
uint requestedSampleRate,
|
||||
uint requestedChannelCount)
|
||||
: base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
{
|
||||
_driver = driver;
|
||||
_updateRequiredEvent = driver.GetUpdateRequiredEvent();
|
||||
_callbackDelegate = OutputCallback;
|
||||
_bytesPerFrame = BackendHelper.GetSampleSize(requestedSampleFormat) * (int)requestedChannelCount;
|
||||
|
||||
_gcHandle = GCHandle.Alloc(this, GCHandleType.Normal);
|
||||
|
||||
SetupAudioQueue();
|
||||
}
|
||||
|
||||
private void SetupAudioQueue()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
AudioStreamBasicDescription format = AppleHardwareDeviceDriver.GetAudioFormat(
|
||||
RequestedSampleFormat,
|
||||
RequestedSampleRate,
|
||||
RequestedChannelCount);
|
||||
|
||||
nint callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate);
|
||||
nint userData = GCHandle.ToIntPtr(_gcHandle);
|
||||
|
||||
int result = AudioQueueNewOutput(
|
||||
ref format,
|
||||
callbackPtr,
|
||||
userData,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
0,
|
||||
out _audioQueue);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"AudioQueueNewOutput failed: {result}");
|
||||
}
|
||||
|
||||
uint framesPerBuffer = RequestedSampleRate / 100;
|
||||
uint bufferSize = framesPerBuffer * (uint)_bytesPerFrame;
|
||||
|
||||
for (int i = 0; i < NumBuffers; i++)
|
||||
{
|
||||
AudioQueueAllocateBuffer(_audioQueue, bufferSize, out _audioQueueBuffers[i]);
|
||||
_bufferBytesFilled[i] = 0;
|
||||
|
||||
PrimeBuffer(_audioQueueBuffers[i], i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void PrimeBuffer(nint bufferPtr, int bufferIndex)
|
||||
{
|
||||
AudioQueueBuffer* buffer = (AudioQueueBuffer*)bufferPtr;
|
||||
|
||||
int capacityBytes = (int)buffer->AudioDataBytesCapacity;
|
||||
int framesPerBuffer = capacityBytes / _bytesPerFrame;
|
||||
|
||||
int availableFrames = _ringBuffer.Length / _bytesPerFrame;
|
||||
int framesToRead = Math.Min(availableFrames, framesPerBuffer);
|
||||
int bytesToRead = framesToRead * _bytesPerFrame;
|
||||
|
||||
Span<byte> dst = new((void*)buffer->AudioData, capacityBytes);
|
||||
dst.Clear();
|
||||
|
||||
if (bytesToRead > 0)
|
||||
{
|
||||
Span<byte> audio = dst.Slice(0, bytesToRead);
|
||||
_ringBuffer.Read(audio, 0, bytesToRead);
|
||||
ApplyVolume(buffer->AudioData, bytesToRead);
|
||||
}
|
||||
|
||||
buffer->AudioDataByteSize = (uint)capacityBytes;
|
||||
_bufferBytesFilled[bufferIndex] = bytesToRead;
|
||||
|
||||
AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, nint.Zero);
|
||||
}
|
||||
|
||||
private void OutputCallback(nint userData, nint audioQueue, nint bufferPtr)
|
||||
{
|
||||
if (!_started || bufferPtr == nint.Zero)
|
||||
return;
|
||||
|
||||
int bufferIndex = Array.IndexOf(_audioQueueBuffers, bufferPtr);
|
||||
if (bufferIndex < 0)
|
||||
return;
|
||||
|
||||
int bytesPlayed = _bufferBytesFilled[bufferIndex];
|
||||
if (bytesPlayed > 0)
|
||||
{
|
||||
ProcessPlayedSamples(bytesPlayed);
|
||||
}
|
||||
|
||||
PrimeBuffer(bufferPtr, bufferIndex);
|
||||
}
|
||||
|
||||
private void ProcessPlayedSamples(int bytesPlayed)
|
||||
{
|
||||
ulong samplesPlayed = GetSampleCount(bytesPlayed);
|
||||
ulong remaining = samplesPlayed;
|
||||
bool needUpdate = false;
|
||||
|
||||
while (remaining > 0 && _queuedBuffers.TryPeek(out AppleAudioBuffer buffer))
|
||||
{
|
||||
ulong needed = buffer.SampleCount - Interlocked.Read(ref buffer.SamplePlayed);
|
||||
ulong take = Math.Min(needed, remaining);
|
||||
|
||||
ulong played = Interlocked.Add(ref buffer.SamplePlayed, take);
|
||||
remaining -= take;
|
||||
|
||||
if (played == buffer.SampleCount)
|
||||
{
|
||||
_queuedBuffers.TryDequeue(out _);
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
Interlocked.Add(ref _playedSampleCount, take);
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
_updateRequiredEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ApplyVolume(nint dataPtr, int byteSize)
|
||||
{
|
||||
float volume = Math.Clamp(_volume * _driver.Volume, 0f, 1f);
|
||||
if (volume >= 0.999f)
|
||||
return;
|
||||
|
||||
int sampleCount = byteSize / BackendHelper.GetSampleSize(RequestedSampleFormat);
|
||||
|
||||
switch (RequestedSampleFormat)
|
||||
{
|
||||
case SampleFormat.PcmInt16:
|
||||
short* s16 = (short*)dataPtr;
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
s16[i] = (short)(s16[i] * volume);
|
||||
break;
|
||||
|
||||
case SampleFormat.PcmFloat:
|
||||
float* f32 = (float*)dataPtr;
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
f32[i] *= volume;
|
||||
break;
|
||||
|
||||
case SampleFormat.PcmInt32:
|
||||
int* s32 = (int*)dataPtr;
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
s32[i] = (int)(s32[i] * volume);
|
||||
break;
|
||||
|
||||
case SampleFormat.PcmInt8:
|
||||
sbyte* s8 = (sbyte*)dataPtr;
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
s8[i] = (sbyte)(s8[i] * volume);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void QueueBuffer(AudioBuffer buffer)
|
||||
{
|
||||
_ringBuffer.Write(buffer.Data, 0, buffer.Data.Length);
|
||||
_queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.DataPointer, GetSampleCount(buffer)));
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_started)
|
||||
return;
|
||||
|
||||
_started = true;
|
||||
AudioQueueStart(_audioQueue, nint.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Stop()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_started)
|
||||
return;
|
||||
|
||||
_started = false;
|
||||
AudioQueuePause(_audioQueue);
|
||||
}
|
||||
}
|
||||
|
||||
public override ulong GetPlayedSampleCount()
|
||||
=> Interlocked.Read(ref _playedSampleCount);
|
||||
|
||||
public override float GetVolume() => _volume;
|
||||
public override void SetVolume(float volume) => _volume = volume;
|
||||
|
||||
public override bool WasBufferFullyConsumed(AudioBuffer buffer)
|
||||
{
|
||||
if (!_queuedBuffers.TryPeek(out AppleAudioBuffer driverBuffer))
|
||||
return true;
|
||||
|
||||
return driverBuffer.DriverIdentifier != buffer.DataPointer;
|
||||
}
|
||||
|
||||
public override void PrepareToClose() { }
|
||||
public override void UnregisterBuffer(AudioBuffer buffer) { }
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Stop();
|
||||
|
||||
if (_audioQueue != nint.Zero)
|
||||
{
|
||||
AudioQueueStop(_audioQueue, true);
|
||||
AudioQueueDispose(_audioQueue, true);
|
||||
_audioQueue = nint.Zero;
|
||||
}
|
||||
|
||||
if (_gcHandle.IsAllocated)
|
||||
{
|
||||
_gcHandle.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs
Normal file
102
src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Runtime.InteropServices;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace Ryujinx.Audio.Backends.Apple.Native
|
||||
{
|
||||
public static partial class AudioToolbox
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct AudioStreamBasicDescription
|
||||
{
|
||||
public double SampleRate;
|
||||
public uint FormatID;
|
||||
public uint FormatFlags;
|
||||
public uint BytesPerPacket;
|
||||
public uint FramesPerPacket;
|
||||
public uint BytesPerFrame;
|
||||
public uint ChannelsPerFrame;
|
||||
public uint BitsPerChannel;
|
||||
public uint Reserved;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct AudioChannelLayout
|
||||
{
|
||||
public uint AudioChannelLayoutTag;
|
||||
public uint AudioChannelBitmap;
|
||||
public uint NumberChannelDescriptions;
|
||||
}
|
||||
|
||||
internal const uint kAudioFormatLinearPCM = 0x6C70636D;
|
||||
internal const uint kAudioQueueProperty_ChannelLayout = 0x6171636c;
|
||||
internal const uint kAudioChannelLayoutTag_MPEG_5_1_A = 0x650006;
|
||||
internal const uint kAudioFormatFlagIsFloat = (1 << 0);
|
||||
internal const uint kAudioFormatFlagIsSignedInteger = (1 << 2);
|
||||
internal const uint kAudioFormatFlagIsPacked = (1 << 3);
|
||||
internal const uint kAudioFormatFlagIsBigEndian = (1 << 1);
|
||||
internal const uint kAudioFormatFlagIsAlignedHigh = (1 << 4);
|
||||
internal const uint kAudioFormatFlagIsNonInterleaved = (1 << 5);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueNewOutput(
|
||||
ref AudioStreamBasicDescription format,
|
||||
nint callback,
|
||||
nint userData,
|
||||
nint callbackRunLoop,
|
||||
nint callbackRunLoopMode,
|
||||
uint flags,
|
||||
out nint audioQueue);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueSetProperty(
|
||||
nint audioQueue,
|
||||
uint propertyID,
|
||||
ref AudioChannelLayout layout,
|
||||
uint layoutSize);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueDispose(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueAllocateBuffer(
|
||||
nint audioQueue,
|
||||
uint bufferByteSize,
|
||||
out nint buffer);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueStart(nint audioQueue, nint startTime);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueuePause(nint audioQueue);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueStop(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueSetParameter(
|
||||
nint audioQueue,
|
||||
uint parameterID,
|
||||
float value);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueEnqueueBuffer(
|
||||
nint audioQueue,
|
||||
nint buffer,
|
||||
uint numPacketDescs,
|
||||
nint packetDescs);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct AudioQueueBuffer
|
||||
{
|
||||
public uint AudioDataBytesCapacity;
|
||||
public nint AudioData;
|
||||
public uint AudioDataByteSize;
|
||||
public nint UserData;
|
||||
public uint PacketDescriptionCapacity;
|
||||
public nint PacketDescriptions;
|
||||
public uint PacketDescriptionCount;
|
||||
}
|
||||
|
||||
internal const uint kAudioQueueParam_Volume = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -10,7 +10,8 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.OpenAL
|
||||
{
|
||||
public class OpenALHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public sealed class OpenALHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
{
|
||||
private readonly ALDevice _device;
|
||||
private readonly ALContext _context;
|
||||
@@ -148,7 +149,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,8 @@ using System.Threading;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.OpenAL
|
||||
{
|
||||
class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
// ReSharper disable once InconsistentNaming
|
||||
sealed class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
{
|
||||
private readonly OpenALHardwareDeviceDriver _driver;
|
||||
private readonly int _sourceId;
|
||||
@@ -190,7 +191,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _driver.Unregister(this))
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Backends.SDL3
|
||||
|
||||
using unsafe SDL_AudioStreamCallbackPointer = delegate* unmanaged[Cdecl]<nint, SDL_AudioStream*, int, int, void>;
|
||||
|
||||
public class SDL3HardwareDeviceDriver : IHardwareDeviceDriver
|
||||
public sealed class SDL3HardwareDeviceDriver : IHardwareDeviceDriver
|
||||
{
|
||||
private readonly ManualResetEvent _updateRequiredEvent;
|
||||
private readonly ManualResetEvent _pauseEvent;
|
||||
@@ -162,7 +162,7 @@ namespace Ryujinx.Audio.Backends.SDL3
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
@@ -12,10 +12,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.SDL3
|
||||
{
|
||||
|
||||
|
||||
|
||||
unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
sealed unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
{
|
||||
private readonly SDL3HardwareDeviceDriver _driver;
|
||||
private readonly ConcurrentQueue<SDL3AudioBuffer> _queuedBuffers;
|
||||
@@ -226,7 +223,7 @@ namespace Ryujinx.Audio.Backends.SDL3
|
||||
return driverBuffer.DriverIdentifier != buffer.DataPointer;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _driver.Unregister(this))
|
||||
{
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native
|
||||
unsafe
|
||||
{
|
||||
int* frameCountPtr = &nativeFrameCount;
|
||||
IntPtr* arenasPtr = &arenas;
|
||||
nint* arenasPtr = &arenas;
|
||||
CheckError(soundio_outstream_begin_write(_context, (nint)arenasPtr, (nint)frameCountPtr));
|
||||
|
||||
frameCount = *frameCountPtr;
|
||||
|
||||
@@ -10,7 +10,7 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.SoundIo
|
||||
{
|
||||
public class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
public sealed class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
{
|
||||
private readonly SoundIoContext _audioContext;
|
||||
private readonly SoundIoDeviceContext _audioDevice;
|
||||
@@ -227,7 +227,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.SoundIo
|
||||
{
|
||||
class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
sealed class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
{
|
||||
private readonly SoundIoHardwareDeviceDriver _driver;
|
||||
private readonly ConcurrentQueue<SoundIoAudioBuffer> _queuedBuffers;
|
||||
@@ -428,7 +428,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _driver.Unregister(this))
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Renderer.Common
|
||||
public uint MixesSize;
|
||||
public uint SinksSize;
|
||||
public uint PerformanceBufferSize;
|
||||
public uint Unknown24;
|
||||
public uint SplitterSize;
|
||||
public uint RenderInfoSize;
|
||||
|
||||
#pragma warning disable IDE0051, CS0169 // Remove unused field
|
||||
|
||||
@@ -433,8 +433,12 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
public ResultCode UpdateSplitter(SplitterContext context)
|
||||
{
|
||||
long initialInputConsumed = _inputReader.Consumed;
|
||||
|
||||
if (context.Update(ref _inputReader))
|
||||
{
|
||||
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.SplitterSize);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}'!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ namespace ARMeilleure.Common
|
||||
/// <summary>
|
||||
/// Base address for the page.
|
||||
/// </summary>
|
||||
public readonly IntPtr Address;
|
||||
public readonly nint Address;
|
||||
|
||||
public AddressTablePage(bool isSparse, IntPtr address)
|
||||
public AddressTablePage(bool isSparse, nint address)
|
||||
{
|
||||
IsSparse = isSparse;
|
||||
Address = address;
|
||||
@@ -47,20 +47,20 @@ namespace ARMeilleure.Common
|
||||
public readonly SparseMemoryBlock Block;
|
||||
private readonly TrackingEventDelegate _trackingEvent;
|
||||
|
||||
public TableSparseBlock(ulong size, Action<IntPtr> ensureMapped, PageInitDelegate pageInit)
|
||||
public TableSparseBlock(ulong size, Action<nint> ensureMapped, PageInitDelegate pageInit)
|
||||
{
|
||||
SparseMemoryBlock block = new(size, pageInit, null);
|
||||
|
||||
_trackingEvent = (address, size, write) =>
|
||||
{
|
||||
ulong pointer = (ulong)block.Block.Pointer + address;
|
||||
ensureMapped((IntPtr)pointer);
|
||||
ensureMapped((nint)pointer);
|
||||
return pointer;
|
||||
};
|
||||
|
||||
bool added = NativeSignalHandler.AddTrackedRegion(
|
||||
(nuint)block.Block.Pointer,
|
||||
(nuint)(block.Block.Pointer + (IntPtr)block.Block.Size),
|
||||
(nuint)(block.Block.Pointer + (nint)block.Block.Size),
|
||||
Marshal.GetFunctionPointerForDelegate(_trackingEvent));
|
||||
|
||||
if (!added)
|
||||
@@ -116,7 +116,7 @@ namespace ARMeilleure.Common
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr Base
|
||||
public nint Base
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -124,7 +124,7 @@ namespace ARMeilleure.Common
|
||||
|
||||
lock (_pages)
|
||||
{
|
||||
return (IntPtr)GetRootPage();
|
||||
return (nint)GetRootPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,7 @@ namespace ARMeilleure.Common
|
||||
|
||||
long index = Levels[^1].GetValue(address);
|
||||
|
||||
EnsureMapped((IntPtr)(page + index));
|
||||
EnsureMapped((nint)(page + index));
|
||||
|
||||
return ref page[index];
|
||||
}
|
||||
@@ -284,7 +284,7 @@ namespace ARMeilleure.Common
|
||||
/// Ensure the given pointer is mapped in any overlapping sparse reservations.
|
||||
/// </summary>
|
||||
/// <param name="ptr">Pointer to be mapped</param>
|
||||
private void EnsureMapped(IntPtr ptr)
|
||||
private void EnsureMapped(nint ptr)
|
||||
{
|
||||
if (Sparse)
|
||||
{
|
||||
@@ -299,7 +299,7 @@ namespace ARMeilleure.Common
|
||||
{
|
||||
SparseMemoryBlock sparse = reserved.Block;
|
||||
|
||||
if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size)
|
||||
if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (nint)sparse.Block.Size)
|
||||
{
|
||||
sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer));
|
||||
|
||||
@@ -319,15 +319,15 @@ namespace ARMeilleure.Common
|
||||
/// </summary>
|
||||
/// <param name="level">Level to get the fill value for</param>
|
||||
/// <returns>The fill value</returns>
|
||||
private IntPtr GetFillValue(int level)
|
||||
private nint GetFillValue(int level)
|
||||
{
|
||||
if (_fillBottomLevel != null && level == Levels.Length - 2)
|
||||
{
|
||||
return (IntPtr)_fillBottomLevelPtr;
|
||||
return (nint)_fillBottomLevelPtr;
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
return nint.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ namespace ARMeilleure.Common
|
||||
/// <param name="fill">Fill value</param>
|
||||
/// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param>
|
||||
/// <returns>Allocated block</returns>
|
||||
private IntPtr Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
|
||||
private nint Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
|
||||
{
|
||||
int size = sizeof(T) * length;
|
||||
|
||||
@@ -405,7 +405,7 @@ namespace ARMeilleure.Common
|
||||
}
|
||||
}
|
||||
|
||||
page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset);
|
||||
page = new AddressTablePage(true, block.Block.Pointer + (nint)_sparseReservedOffset);
|
||||
|
||||
_sparseReservedOffset += (ulong)size;
|
||||
|
||||
@@ -413,7 +413,7 @@ namespace ARMeilleure.Common
|
||||
}
|
||||
else
|
||||
{
|
||||
IntPtr address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size);
|
||||
nint address = (nint)NativeAllocator.Instance.Allocate((uint)size);
|
||||
page = new AddressTablePage(false, address);
|
||||
|
||||
Span<T> span = new((void*)page.Address, length);
|
||||
|
||||
@@ -658,7 +658,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel;
|
||||
|
||||
IntPtr hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
|
||||
nint hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
|
||||
|
||||
if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size))
|
||||
{
|
||||
|
||||
12
src/Ryujinx.Graphics.RenderDocApi/Capture.cs
Normal file
12
src/Ryujinx.Graphics.RenderDocApi/Capture.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public readonly record struct Capture(int Index, string FileName, DateTime Timestamp)
|
||||
{
|
||||
public void SetComments(string comments)
|
||||
{
|
||||
RenderDoc.SetCaptureFileComments(FileName, comments);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs
Normal file
100
src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public enum CaptureOption
|
||||
{
|
||||
/// <summary>
|
||||
/// specifies whether the application is allowed to enable vsync. Default is on.
|
||||
/// </summary>
|
||||
AllowVsync = 0,
|
||||
/// <summary>
|
||||
/// specifies whether the application is allowed to enter exclusive fullscreen. Default is on.
|
||||
/// </summary>
|
||||
AllowFullscreen = 1,
|
||||
/// <summary>
|
||||
/// specifies whether (where possible) API-specific debugging is enabled. Default is off.
|
||||
/// </summary>
|
||||
ApiValidation = 2,
|
||||
/// <summary>
|
||||
/// specifies whether each API call should save a callstack. Default is off.
|
||||
/// </summary>
|
||||
CaptureCallstacks = 3,
|
||||
/// <summary>
|
||||
/// specifies whether, if <see cref="CaptureCallstacks"/> is enabled, callstacks are only saved on actions. Default is off.
|
||||
/// </summary>
|
||||
CaptureCallstacksOnlyDraws = 4,
|
||||
/// <summary>
|
||||
/// specifies a delay in seconds after launching a process to pause, to allow debuggers to attach. <br/>
|
||||
/// This will only apply to child processes since the delay happens at process startup. Default is 0.
|
||||
/// </summary>
|
||||
DelayForDebugger = 5,
|
||||
/// <summary>
|
||||
/// specifies whether any mapped memory updates should be bounds-checked for overruns,
|
||||
/// and uninitialised buffers are initialized to <code>0xDDDDDDDD</code> to catch use of uninitialised data.
|
||||
/// Only supported on D3D11 and OpenGL. Default is off.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do
|
||||
/// not do the same kind of interception & checking, and undefined contents are really undefined.
|
||||
/// </remarks>
|
||||
VerifyBufferAccess = 6,
|
||||
/// <summary>
|
||||
/// Hooks any system API calls that create child processes, and injects
|
||||
/// RenderDoc into them recursively with the same options.
|
||||
/// </summary>
|
||||
HookIntoChildren = 7,
|
||||
/// <summary>
|
||||
/// specifies whether all live resources at the time of capture should be included in the capture,
|
||||
/// even if they are not referenced by the frame. Default is off.
|
||||
/// </summary>
|
||||
RefAllSources = 8,
|
||||
/// <summary>
|
||||
/// By default, RenderDoc skips saving initial states for resources where the
|
||||
/// previous contents don't appear to be used, assuming that writes before
|
||||
/// reads indicate previous contents aren't used.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or
|
||||
/// getting it will be ignored, to allow compatibility with older versions.
|
||||
/// In v1.1 the option acts as if it's always enabled.
|
||||
/// </remarks>
|
||||
SaveAllInitials = 9,
|
||||
/// <summary>
|
||||
/// In APIs that allow for the recording of command lists to be replayed later,
|
||||
/// RenderDoc may choose to not capture command lists before a frame capture is
|
||||
/// triggered, to reduce overheads. This means any command lists recorded once
|
||||
/// and replayed many times will not be available and may cause a failure to
|
||||
/// capture.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// NOTE: This is only true for APIs where multithreading is difficult or
|
||||
/// discouraged. Newer APIs like Vulkan and D3D12 will ignore this option
|
||||
/// and always capture all command lists since the API is heavily oriented
|
||||
/// around it and the overheads have been reduced by API design.
|
||||
/// </remarks>
|
||||
CaptureAllCmdLists = 10,
|
||||
/// <summary>
|
||||
/// Mute API debugging output when the <see cref="ApiValidation"/> option is enabled.
|
||||
/// </summary>
|
||||
DebugOutputMute = 11,
|
||||
/// <summary>
|
||||
/// Allow vendor extensions to be used even when they may be
|
||||
/// incompatible with RenderDoc and cause corrupted replays or crashes.
|
||||
/// </summary>
|
||||
AllowUnsupportedVendorExtensions = 12,
|
||||
/// <summary>
|
||||
/// Define a soft memory limit which some APIs may aim to keep overhead under where
|
||||
/// possible. Anything above this limit will where possible be saved directly to disk during
|
||||
/// capture.<br/>
|
||||
/// This will cause increased disk space use (which may cause a capture to fail if disk space is
|
||||
/// exhausted) as well as slower capture times.
|
||||
/// <br/><br/>
|
||||
/// Not all memory allocations may be deferred like this so it is not a guarantee of a memory
|
||||
/// limit.
|
||||
/// <br/><br/>
|
||||
/// Units are in MBs, suggested values would range from 200MB to 1000MB.
|
||||
/// </summary>
|
||||
SoftMemoryLimit = 13,
|
||||
}
|
||||
}
|
||||
83
src/Ryujinx.Graphics.RenderDocApi/InputButton.cs
Normal file
83
src/Ryujinx.Graphics.RenderDocApi/InputButton.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
|
||||
// ReSharper disable UnusedMember.Global
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public enum InputButton
|
||||
{
|
||||
// '0' - '9' matches ASCII values
|
||||
Key0 = 0x30,
|
||||
Key1 = 0x31,
|
||||
Key2 = 0x32,
|
||||
Key3 = 0x33,
|
||||
Key4 = 0x34,
|
||||
Key5 = 0x35,
|
||||
Key6 = 0x36,
|
||||
Key7 = 0x37,
|
||||
Key8 = 0x38,
|
||||
Key9 = 0x39,
|
||||
|
||||
// 'A' - 'Z' matches ASCII values
|
||||
A = 0x41,
|
||||
B = 0x42,
|
||||
C = 0x43,
|
||||
D = 0x44,
|
||||
E = 0x45,
|
||||
F = 0x46,
|
||||
G = 0x47,
|
||||
H = 0x48,
|
||||
I = 0x49,
|
||||
J = 0x4A,
|
||||
K = 0x4B,
|
||||
L = 0x4C,
|
||||
M = 0x4D,
|
||||
N = 0x4E,
|
||||
O = 0x4F,
|
||||
P = 0x50,
|
||||
Q = 0x51,
|
||||
R = 0x52,
|
||||
S = 0x53,
|
||||
T = 0x54,
|
||||
U = 0x55,
|
||||
V = 0x56,
|
||||
W = 0x57,
|
||||
X = 0x58,
|
||||
Y = 0x59,
|
||||
Z = 0x5A,
|
||||
|
||||
// leave the rest of the ASCII range free
|
||||
// in case we want to use it later
|
||||
NonPrintable = 0x100,
|
||||
|
||||
Divide,
|
||||
Multiply,
|
||||
Subtract,
|
||||
Plus,
|
||||
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
|
||||
Home,
|
||||
End,
|
||||
Insert,
|
||||
Delete,
|
||||
PageUp,
|
||||
PageDn,
|
||||
|
||||
Backspace,
|
||||
Tab,
|
||||
PrtScrn,
|
||||
Pause,
|
||||
|
||||
Max,
|
||||
}
|
||||
}
|
||||
39
src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs
Normal file
39
src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
[Flags]
|
||||
public enum OverlayBits
|
||||
{
|
||||
/// <summary>
|
||||
/// This single bit controls whether the overlay is enabled or disabled globally
|
||||
/// </summary>
|
||||
Enabled = 1 << 0,
|
||||
/// <summary>
|
||||
/// Show the average framerate over several seconds as well as min/max
|
||||
/// </summary>
|
||||
FrameRate = 1 << 1,
|
||||
/// <summary>
|
||||
/// Show the current frame number
|
||||
/// </summary>
|
||||
FrameNumber = 1 << 2,
|
||||
/// <summary>
|
||||
/// Show a list of recent captures, and how many captures have been made
|
||||
/// </summary>
|
||||
CaptureList = 1 << 3,
|
||||
/// <summary>
|
||||
/// Default values for the overlay mask
|
||||
/// </summary>
|
||||
Default = Enabled | FrameRate | FrameNumber | CaptureList,
|
||||
/// <summary>
|
||||
/// Enable all bits
|
||||
/// </summary>
|
||||
All = ~0,
|
||||
/// <summary>
|
||||
/// Disable all bits
|
||||
/// </summary>
|
||||
None = 0
|
||||
}
|
||||
}
|
||||
5
src/Ryujinx.Graphics.RenderDocApi/README.md
Normal file
5
src/Ryujinx.Graphics.RenderDocApi/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Ryujinx.Graphics.RenderDocApi
|
||||
|
||||
This is a C# binding for RenderDoc's application API.
|
||||
This is a source-inclusion of https://github.com/utkumaden/RenderdocSharp.
|
||||
I didn't use the NuGet package as I had a few minor changes I wanted to make, and I want to learn from it as well via hands-on experience.
|
||||
639
src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs
Normal file
639
src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs
Normal file
@@ -0,0 +1,639 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public static unsafe partial class RenderDoc
|
||||
{
|
||||
/// <summary>
|
||||
/// True if the API is available.
|
||||
/// </summary>
|
||||
public static bool IsAvailable => Api != null;
|
||||
|
||||
/// <summary>
|
||||
/// Set the minimum version of the API you require.
|
||||
/// </summary>
|
||||
/// <remarks>Set this before you do anything else with the RenderDoc API, including <see cref="IsAvailable"/>.</remarks>
|
||||
public static RenderDocVersion MinimumRequired { get; set; } = RenderDocVersion.Version_1_0_0;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to assert versions.
|
||||
/// </summary>
|
||||
public static bool AssertVersionEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the API available.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(IsAvailable))]
|
||||
public static Version? Version
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsAvailable)
|
||||
return null;
|
||||
|
||||
int major, minor, build;
|
||||
Api->GetApiVersion(&major, &minor, &build);
|
||||
return new Version(major, minor, build);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current mask which determines what sections of the overlay render on each window.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static OverlayBits OverlayBits
|
||||
{
|
||||
get => Api->GetOverlayBits();
|
||||
set
|
||||
{
|
||||
Api->MaskOverlayBits(~value, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The template for new captures.<br/>
|
||||
/// The template can either be a relative or absolute path, which determines where captures will be saved and how they will be named.
|
||||
/// If the path template is 'my_captures/example', then captures saved will be e.g.
|
||||
/// 'my_captures/example_frame123.rdc' and 'my_captures/example_frame456.rdc'.<br/>
|
||||
/// Relative paths will be saved relative to the process’s current working directory.<br/>
|
||||
/// </summary>
|
||||
/// <remarks>The default template is in a folder controlled by the UI - initially the system temporary folder, and the filename is the executable’s filename.</remarks>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static string CaptureFilePathTemplate
|
||||
{
|
||||
get
|
||||
{
|
||||
byte* ptr = Api->GetCaptureFilePathTemplate();
|
||||
return Marshal.PtrToStringUTF8((nint)ptr)!;
|
||||
}
|
||||
set
|
||||
{
|
||||
fixed (byte* ptr = value.ToNullTerminatedByteArray())
|
||||
{
|
||||
Api->SetCaptureFilePathTemplate(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The amount of frame captures that have been made.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static int CaptureCount => Api->GetNumCaptures();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the RenderDoc UI is currently connected to this process.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool IsTargetControlConnected => Api is not null && Api->IsTargetControlConnected() != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current frame is capturing.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool IsFrameCapturing => Api is not null && Api->IsFrameCapturing() != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Set one of the options for tweaking some behaviors of capturing.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be set.</param>
|
||||
/// <param name="integer">the unsigned integer value to set for the option.</param>
|
||||
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
|
||||
/// <returns>
|
||||
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
|
||||
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool SetCaptureOption(CaptureOption option, uint integer)
|
||||
{
|
||||
return Api is not null && Api->SetCaptureOptionU32(option, integer) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set one of the options for tweaking some behaviors of capturing.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be set.</param>
|
||||
/// <param name="boolean">the value to set for the option, converted to a 0 or 1 before setting.</param>
|
||||
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
|
||||
/// <returns>
|
||||
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
|
||||
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool SetCaptureOption(CaptureOption option, bool boolean)
|
||||
=> SetCaptureOption(option, boolean ? 1 : 0);
|
||||
|
||||
/// <summary>
|
||||
/// Set one of the options for tweaking some behaviors of capturing.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be set.</param>
|
||||
/// <param name="single">the floating point value to set for the option.</param>
|
||||
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
|
||||
/// <returns>
|
||||
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
|
||||
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool SetCaptureOption(CaptureOption option, float single)
|
||||
{
|
||||
return Api is not null && Api->SetCaptureOptionF32(option, single) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <param name="integer">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, <see cref="int.MaxValue"/>.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void GetCaptureOption(CaptureOption option, out uint integer)
|
||||
{
|
||||
integer = Api->GetCaptureOptionU32(option);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <param name="single">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, -<see cref="float.MaxValue"/>.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void GetCaptureOption(CaptureOption option, out float single)
|
||||
{
|
||||
single = Api->GetCaptureOptionF32(option);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>,
|
||||
/// converted to a boolean.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <returns>
|
||||
/// the value of the capture option, converted to bool, if the option is a valid <see cref="CaptureOption"/> enum.
|
||||
/// Otherwise, returns null.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool? GetCaptureOptionBool(CaptureOption option)
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
uint returnVal = GetCaptureOptionU32(option);
|
||||
if (returnVal == uint.MaxValue)
|
||||
return null;
|
||||
|
||||
return returnVal is not 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <returns>
|
||||
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
|
||||
/// Otherwise, returns <see cref="int.MaxValue"/>.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static uint GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <returns>
|
||||
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
|
||||
/// Otherwise, returns -<see cref="float.MaxValue"/>.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static float GetCaptureOptionF32(CaptureOption option) => Api->GetCaptureOptionF32(option);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the key bindings in-application for changing the focussed window.
|
||||
/// </summary>
|
||||
/// <param name="buttons">lists the keys to bind.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void SetFocusToggleKeys(ReadOnlySpan<InputButton> buttons)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
fixed (InputButton* ptr = buttons)
|
||||
{
|
||||
Api->SetFocusToggleKeys(ptr, buttons.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the key bindings in-application for triggering a capture on the current window.
|
||||
/// </summary>
|
||||
/// <param name="buttons">lists the keys to bind.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void SetCaptureKeys(ReadOnlySpan<InputButton> buttons)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
fixed (InputButton* ptr = buttons)
|
||||
{
|
||||
Api->SetCaptureKeys(ptr, buttons.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to remove RenderDoc and its hooks from the target process.<br/>
|
||||
/// It must be called as early as possible in the process, and will have undefined results
|
||||
/// if any graphics API functions have been called.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void RemoveHooks()
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->RemoveHooks();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove RenderDoc’s crash handler from the target process.<br/>
|
||||
/// If you have your own crash handler that you want to handle any exceptions,
|
||||
/// RenderDoc’s handler could interfere; so it can be disabled.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void UnloadCrashHandler()
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->UnloadCrashHandler();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger a capture as if the user had pressed one of the capture hotkeys.<br/>
|
||||
/// The capture will be taken from the next frame presented to whichever window is considered current.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void TriggerCapture()
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->TriggerCapture();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the details of all frame capture in the current session.
|
||||
/// This simply calls <see cref="GetCapture"/> for each index available as specified by <see cref="CaptureCount"/>.
|
||||
/// </summary>
|
||||
/// <returns>An immutable array of structs representing RenderDoc Captures.</returns>
|
||||
public static ImmutableArray<Capture> GetCaptures()
|
||||
{
|
||||
if (Api is null) return [];
|
||||
int captureCount = CaptureCount;
|
||||
if (captureCount is 0) return [];
|
||||
|
||||
ImmutableArray<Capture>.Builder captures = ImmutableArray.CreateBuilder<Capture>(captureCount);
|
||||
|
||||
for (int captureIndex = 0; captureIndex < captureCount; captureIndex++)
|
||||
{
|
||||
if (GetCapture(captureIndex) is { } capture)
|
||||
captures.Add(capture);
|
||||
}
|
||||
|
||||
return captures.DrainToImmutable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the details of a particular frame capture, as specified by an index from 0 to <see cref="CaptureCount"/> - 1.
|
||||
/// </summary>
|
||||
/// <param name="index">specifies which capture to return the details of. Must be less than the value returned by <see cref="CaptureCount"/>.</param>
|
||||
/// <returns>A struct representing a RenderDoc Capture.</returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static Capture? GetCapture(int index)
|
||||
{
|
||||
if (Api is null) return null;
|
||||
|
||||
int length = 0;
|
||||
if (Api->GetCapture(index, null, &length, null) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[length + 1];
|
||||
long timestamp;
|
||||
|
||||
fixed (byte* ptr = bytes)
|
||||
Api->GetCapture(index, ptr, &length, ×tamp);
|
||||
|
||||
string fileName = Encoding.UTF8.GetString(bytes[length..]);
|
||||
return new Capture(index, fileName, DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the closest matching replay UI executable for the current RenderDoc module, and launch it.
|
||||
/// </summary>
|
||||
/// <param name="connectTargetControl">if the UI should immediately connect to the application.</param>
|
||||
/// <param name="commandLine">string to be appended to the command line, e.g. a capture filename. If this parameter is null, the command line will be unmodified.</param>
|
||||
/// <returns>true if the UI was successfully launched; false otherwise.</returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool LaunchReplayUI(bool connectTargetControl, string? commandLine = null)
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
if (commandLine == null)
|
||||
{
|
||||
return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, null) != 0;
|
||||
}
|
||||
|
||||
fixed (byte* ptr = commandLine.ToNullTerminatedByteArray())
|
||||
{
|
||||
return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, ptr) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly sets which window is considered active.<br/>
|
||||
/// The active window is the one that will be captured when the keybind to trigger a capture is pressed.
|
||||
/// </summary>
|
||||
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. Must be valid.</param>
|
||||
/// <param name="hWindow">a handle to the platform window handle that will be set active. Must be valid.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void SetActiveWindow(nint hDevice, nint hWindow)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->SetActiveWindow((void*)hDevice, (void*)hWindow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately begin a capture for the specified device/window combination.
|
||||
/// </summary>
|
||||
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void StartFrameCapture(nint hDevice, nint hWindow)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->StartFrameCapture((void*)hDevice, (void*)hWindow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately end an active capture for the specified device/window combination.
|
||||
/// </summary>
|
||||
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <returns>true if the capture succeeded; false otherwise.</returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool EndFrameCapture(nint hDevice, nint hWindow)
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
return Api->EndFrameCapture((void*)hDevice, (void*)hWindow) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger multiple sequential frame captures as if the user had pressed one of the capture hotkeys before each frame.<br/>
|
||||
/// The captures will be taken from the next frames presented to whichever window is considered current.<br/>
|
||||
/// Each capture will be taken independently and saved to a separate file, with no reference to the other frames.
|
||||
/// </summary>
|
||||
/// <param name="numFrames">the number of frames to capture.</param>
|
||||
/// <remarks>Requires RenderDoc API version 1.1</remarks>
|
||||
[RenderDocApiVersion(1, 1)]
|
||||
public static void TriggerMultiFrameCapture(uint numFrames)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
AssertAtLeast(1, 1);
|
||||
Api->TriggerMultiFrameCapture(numFrames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an arbitrary comments field to the most recent capture,
|
||||
/// which will then be displayed in the UI to anyone opening the capture.
|
||||
/// <br/><br/>
|
||||
/// This is equivalent to calling <see cref="SetCaptureFileComments"/> with a null first (fileName) parameter.
|
||||
/// </summary>
|
||||
/// <param name="comments">the comments to set in the capture file.</param>
|
||||
/// <remarks>Requires RenderDoc API version 1.2</remarks>
|
||||
public static void SetMostRecentCaptureFileComments(string comments)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
AssertAtLeast(1, 2);
|
||||
|
||||
byte[] commentBytes = comments.ToNullTerminatedByteArray();
|
||||
|
||||
fixed (byte* pcomment = commentBytes)
|
||||
{
|
||||
Api->SetCaptureFileComments((byte*)nint.Zero, pcomment);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an arbitrary comments field to an existing capture on disk,
|
||||
/// which will then be displayed in the UI to anyone opening the capture.
|
||||
/// </summary>
|
||||
/// <param name="fileName">the path to the capture file to set comments in. If this path is null or an empty string, the most recent capture file that has been created will be used.</param>
|
||||
/// <param name="comments">the comments to set in the capture file.</param>
|
||||
/// <remarks>Requires RenderDoc API version 1.2</remarks>
|
||||
[RenderDocApiVersion(1, 2)]
|
||||
public static void SetCaptureFileComments(string? fileName, string comments)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
AssertAtLeast(1, 2);
|
||||
|
||||
byte[] commentBytes = comments.ToNullTerminatedByteArray();
|
||||
|
||||
fixed (byte* pcomment = commentBytes)
|
||||
{
|
||||
if (fileName is null)
|
||||
{
|
||||
Api->SetCaptureFileComments((byte*)nint.Zero, pcomment);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] fileBytes = fileName.ToNullTerminatedByteArray();
|
||||
|
||||
fixed (byte* pfile = fileBytes)
|
||||
{
|
||||
Api->SetCaptureFileComments(pfile, pcomment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Similar to <see cref="EndFrameCapture"/>, but the capture contents will be discarded immediately, and not processed and written to disk.<br/>
|
||||
/// This will be more efficient than <see cref="EndFrameCapture"/> if the frame capture is not needed.
|
||||
/// </summary>
|
||||
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <returns>true if the capture was discarded; false if there was an error or no capture was in progress.</returns>
|
||||
/// <remarks>Requires RenderDoc API version 1.4</remarks>
|
||||
[RenderDocApiVersion(1, 4)]
|
||||
public static bool DiscardFrameCapture(nint hDevice, nint hWindow)
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
AssertAtLeast(1, 4);
|
||||
return Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow) != 0;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Requests that the currently connected replay UI raise its window to the top.<br/>
|
||||
/// This is only possible if an instance of the replay UI is currently connected, otherwise this method does nothing.<br/>
|
||||
/// This can be used in conjunction with <see cref="IsTargetControlConnected"/> and <see cref="LaunchReplayUI"/>,<br/> to intelligently handle showing the UI after making a capture.<br/><br/>
|
||||
/// Given OS differences, it is not guaranteed that the UI will be successfully raised even if the request is passed on.<br/>
|
||||
/// On some systems it may only be highlighted or otherwise indicated to the user.
|
||||
/// </summary>
|
||||
/// <returns>true if the request was passed onto the UI successfully; false if there is no UI connected or some other error occurred.</returns>
|
||||
/// <remarks>Requires RenderDoc API version 1.5</remarks>
|
||||
[RenderDocApiVersion(1, 5)]
|
||||
public static bool ShowReplayUI()
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
AssertAtLeast(1, 5);
|
||||
return Api->ShowReplayUI() != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a given title for the currently in-progress capture, which will be displayed in the UI.<br/>
|
||||
/// This can be used either with a user-defined capture using a manual start and end,
|
||||
/// or an automatic capture triggered by <see cref="TriggerCapture"/> or a keypress.<br/>
|
||||
/// If multiple captures are ongoing at once, the title will be applied to the first capture to end only.<br/>
|
||||
/// Any subsequent captures will not get any title unless the function is called again.
|
||||
/// This function can only be called while a capture is in-progress,
|
||||
/// after <see cref="StartFrameCapture"/> and before <see cref="EndFrameCapture"/>.<br/>
|
||||
/// If it is called elsewhere it will have no effect.
|
||||
/// If it is called multiple times within a capture, only the last title will have any effect.
|
||||
/// </summary>
|
||||
/// <param name="title">The title to set for the in-progress capture.</param>
|
||||
/// <remarks>Requires RenderDoc API version 1.6</remarks>
|
||||
[RenderDocApiVersion(1, 6)]
|
||||
public static void SetCaptureTitle(string title)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
AssertAtLeast(1, 6);
|
||||
fixed (byte* ptr = title.ToNullTerminatedByteArray())
|
||||
Api->SetCaptureTitle(ptr);
|
||||
}
|
||||
|
||||
#region Dynamic Library loading
|
||||
|
||||
/// <summary>
|
||||
/// Reload the internal RenderDoc API structure. Useful for manually refreshing <see cref="Api"/> while using process injection.
|
||||
/// </summary>
|
||||
/// <param name="ignoreAlreadyLoaded">Ignores the existing API function structure and overwrites it with a re-request.</param>
|
||||
/// <param name="requiredVersion">The version of the RenderDoc API required by your application.</param>
|
||||
public static void ReloadApi(bool ignoreAlreadyLoaded = false, RenderDocVersion? requiredVersion = null)
|
||||
{
|
||||
if (_loaded && !ignoreAlreadyLoaded)
|
||||
return;
|
||||
|
||||
lock (typeof(RenderDoc))
|
||||
{
|
||||
// Prevent double loads.
|
||||
if (_loaded && !ignoreAlreadyLoaded)
|
||||
return;
|
||||
|
||||
if (requiredVersion.HasValue)
|
||||
MinimumRequired = requiredVersion.Value;
|
||||
|
||||
_loaded = true;
|
||||
_api = GetApi(MinimumRequired);
|
||||
|
||||
if (_api != null)
|
||||
AssertAtLeast(MinimumRequired);
|
||||
}
|
||||
}
|
||||
|
||||
private static RenderDocApi* _api = null;
|
||||
private static bool _loaded;
|
||||
|
||||
private static RenderDocApi* Api
|
||||
{
|
||||
get
|
||||
{
|
||||
ReloadApi();
|
||||
return _api;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Regex _dynamicLibraryPattern = RenderDocApiDynamicLibraryRegex();
|
||||
|
||||
private static RenderDocApi* GetApi(RenderDocVersion minimumRequired = RenderDocVersion.Version_1_0_0)
|
||||
{
|
||||
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
|
||||
{
|
||||
string moduleName = module.FileName ?? string.Empty;
|
||||
|
||||
if (!_dynamicLibraryPattern.IsMatch(moduleName))
|
||||
continue;
|
||||
|
||||
if (!NativeLibrary.TryLoad(moduleName, out nint moduleHandle))
|
||||
return null;
|
||||
|
||||
if (!NativeLibrary.TryGetExport(moduleHandle, "RENDERDOC_GetAPI", out nint procAddress))
|
||||
return null;
|
||||
|
||||
var RENDERDOC_GetApi = (delegate* unmanaged[Cdecl]<RenderDocVersion, RenderDocApi**, int>)procAddress;
|
||||
|
||||
RenderDocApi* api;
|
||||
return RENDERDOC_GetApi(minimumRequired, &api) != 0 ? api : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void AssertAtLeast(RenderDocVersion rdv, [CallerMemberName] string callee = "")
|
||||
{
|
||||
Version ver = rdv.SystemVersion;
|
||||
AssertAtLeast(ver.Major, ver.Minor, ver.Build, callee);
|
||||
}
|
||||
|
||||
private static void AssertAtLeast(int major, int minor, int patch = 0, [CallerMemberName] string callee = "")
|
||||
{
|
||||
if (!AssertVersionEnabled)
|
||||
return;
|
||||
|
||||
if (Version!.Major < major)
|
||||
goto fail;
|
||||
|
||||
if (Version.Major > major)
|
||||
goto success;
|
||||
if (Version.Minor < minor)
|
||||
goto fail;
|
||||
if (Version.Minor > minor)
|
||||
goto success;
|
||||
if (Version.Build < patch)
|
||||
goto fail;
|
||||
|
||||
success:
|
||||
return;
|
||||
|
||||
fail:
|
||||
Version minVersion =
|
||||
typeof(RenderDoc).GetMethod(callee)!.GetCustomAttribute<RenderDocApiVersionAttribute>()!.MinVersion;
|
||||
throw new NotSupportedException(
|
||||
$"This API was introduced in RenderDoc API {minVersion}. Current API version is {Version}.");
|
||||
}
|
||||
|
||||
private static byte[] ToNullTerminatedByteArray(this string str, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
|
||||
return encoding.GetBytes(str + '\0');
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex RenderDocApiDynamicLibraryRegex();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
51
src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs
Normal file
51
src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
#pragma warning disable CS0649
|
||||
internal unsafe struct RenderDocApi
|
||||
{
|
||||
public delegate* unmanaged[Cdecl]<int*, int*, int*, void> GetApiVersion;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<CaptureOption, uint, int> SetCaptureOptionU32;
|
||||
public delegate* unmanaged[Cdecl]<CaptureOption, float, int> SetCaptureOptionF32;
|
||||
public delegate* unmanaged[Cdecl]<CaptureOption, uint> GetCaptureOptionU32;
|
||||
public delegate* unmanaged[Cdecl]<CaptureOption, float> GetCaptureOptionF32;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetFocusToggleKeys;
|
||||
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetCaptureKeys;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<OverlayBits> GetOverlayBits;
|
||||
public delegate* unmanaged[Cdecl]<OverlayBits, OverlayBits, void> MaskOverlayBits;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<void> RemoveHooks;
|
||||
public delegate* unmanaged[Cdecl]<void> UnloadCrashHandler;
|
||||
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureFilePathTemplate;
|
||||
public delegate* unmanaged[Cdecl]<byte*> GetCaptureFilePathTemplate;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<int> GetNumCaptures;
|
||||
public delegate* unmanaged[Cdecl]<int, byte*, int*, long*, uint> GetCapture;
|
||||
public delegate* unmanaged[Cdecl]<void> TriggerCapture;
|
||||
public delegate* unmanaged[Cdecl]<uint> IsTargetControlConnected;
|
||||
public delegate* unmanaged[Cdecl]<uint, byte*, uint> LaunchReplayUI;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<void*, void*, void> SetActiveWindow;
|
||||
public delegate* unmanaged[Cdecl]<void*, void*, void> StartFrameCapture;
|
||||
public delegate* unmanaged[Cdecl]<uint> IsFrameCapturing;
|
||||
public delegate* unmanaged[Cdecl]<void*, void*, uint> EndFrameCapture;
|
||||
|
||||
// 1.1
|
||||
public delegate* unmanaged[Cdecl]<uint, void> TriggerMultiFrameCapture;
|
||||
|
||||
// 1.2
|
||||
public delegate* unmanaged[Cdecl]<byte*, byte*, void> SetCaptureFileComments;
|
||||
|
||||
// 1.3
|
||||
public delegate* unmanaged[Cdecl]<void*, void*, uint> DiscardFrameCapture;
|
||||
|
||||
// 1.5
|
||||
public delegate* unmanaged[Cdecl]<uint> ShowReplayUI;
|
||||
|
||||
// 1.6
|
||||
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureTitle;
|
||||
}
|
||||
#pragma warning restore CS0649
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
|
||||
public sealed class RenderDocApiVersionAttribute : Attribute
|
||||
{
|
||||
public Version MinVersion { get; }
|
||||
|
||||
public RenderDocApiVersionAttribute(int major, int minor, int patch = 0)
|
||||
{
|
||||
MinVersion = new Version(major, minor, patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs
Normal file
47
src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public enum RenderDocVersion
|
||||
{
|
||||
Version_1_0_0 = 10000,
|
||||
Version_1_0_1 = 10001,
|
||||
Version_1_0_2 = 10002,
|
||||
Version_1_1_0 = 10100,
|
||||
Version_1_1_1 = 10101,
|
||||
Version_1_1_2 = 10102,
|
||||
Version_1_2_0 = 10200,
|
||||
Version_1_3_0 = 10300,
|
||||
Version_1_4_0 = 10400,
|
||||
Version_1_4_1 = 10401,
|
||||
Version_1_4_2 = 10402,
|
||||
Version_1_5_0 = 10500,
|
||||
Version_1_6_0 = 10600,
|
||||
}
|
||||
|
||||
public static partial class Helpers
|
||||
{
|
||||
extension(RenderDocVersion rdv)
|
||||
{
|
||||
public Version SystemVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
int i = (int)rdv;
|
||||
return new (i / 10000, (i % 10000) / 100, i % 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension(Version sv)
|
||||
{
|
||||
public RenderDocVersion RenderDocVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
return (RenderDocVersion)(sv.Major * 10000 + sv.Minor * 100 + sv.Build);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
32
src/Ryujinx.Graphics.Vulkan/Helpers.cs
Normal file
32
src/Ryujinx.Graphics.Vulkan/Helpers.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Silk.NET.Vulkan;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Graphics.Vulkan
|
||||
{
|
||||
public static class Helpers
|
||||
{
|
||||
extension(Vk api)
|
||||
{
|
||||
/// <summary>
|
||||
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
|
||||
/// </summary>
|
||||
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Vk"/>'s <see cref="Instance"/> pointer.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe void* GetRenderDocDevicePointer() =>
|
||||
api.CurrentInstance is not null
|
||||
? api.CurrentInstance.Value.GetRenderDocDevicePointer()
|
||||
: null;
|
||||
}
|
||||
|
||||
extension(Instance instance)
|
||||
{
|
||||
/// <summary>
|
||||
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
|
||||
/// </summary>
|
||||
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Instance"/>'s pointer.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe void* GetRenderDocDevicePointer()
|
||||
=> (*((void**)(instance.Handle)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
IntPtr configSize = (nint)Marshal.SizeOf<MVKConfiguration>();
|
||||
nint configSize = (nint)Marshal.SizeOf<MVKConfiguration>();
|
||||
|
||||
vkGetMoltenVKConfigurationMVK(nint.Zero, out MVKConfiguration config, configSize);
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
enabledExtensions = enabledExtensions.Append(ExtDebugUtils.ExtensionName).ToArray();
|
||||
}
|
||||
|
||||
IntPtr appName = Marshal.StringToHGlobalAnsi(AppName);
|
||||
nint appName = Marshal.StringToHGlobalAnsi(AppName);
|
||||
|
||||
ApplicationInfo applicationInfo = new()
|
||||
{
|
||||
@@ -166,7 +166,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api)
|
||||
{
|
||||
IntPtr appName = Marshal.StringToHGlobalAnsi(AppName);
|
||||
nint appName = Marshal.StringToHGlobalAnsi(AppName);
|
||||
|
||||
ApplicationInfo applicationInfo = new()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,29 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||
|
||||
return needUpdateRight;
|
||||
}
|
||||
|
||||
public bool isAtRest(int playerNumber)
|
||||
{
|
||||
ref NpadInternalState currentNpad = ref _device.Hid.SharedMemory.Npads[playerNumber].InternalState;
|
||||
|
||||
if (currentNpad.StyleSet == NpadStyleTag.None)
|
||||
{
|
||||
return true; // it will always be at rest because it cannot move.
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Cpu;
|
||||
using Ryujinx.HLE.Exceptions;
|
||||
using Ryujinx.HLE.HOS.Ipc;
|
||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||
@@ -14,6 +15,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
@@ -487,6 +489,23 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(106)] // 20.0.0+
|
||||
// SetProtocol
|
||||
public ResultCode SetProtocol(ServiceCtx context)
|
||||
{
|
||||
uint protocolValue = context.RequestData.ReadUInt32();
|
||||
|
||||
// On NX only input value 1 or 3 is allowed, with an error being thrown otherwise.
|
||||
|
||||
if (protocolValue != 1 && protocolValue != 3)
|
||||
{
|
||||
throw new ArgumentException($"{GetType().FullName}: Protocol value is not 1 or 3!! Protocol value: {protocolValue}");
|
||||
}
|
||||
|
||||
Logger.Stub?.PrintStub(LogClass.ServiceLdn, $"Protocol value: {protocolValue}");
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(200)]
|
||||
// OpenAccessPoint()
|
||||
public ResultCode OpenAccessPoint(ServiceCtx context)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
477
src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/IMifare.cs
Normal file
477
src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/IMifare.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
enum State
|
||||
{
|
||||
NonInitialized,
|
||||
Initialized,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
17
src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/ResultCode.cs
Normal file
17
src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/ResultCode.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; }
|
||||
|
||||
public nint Handle => IntPtr.Zero;
|
||||
public nint Handle => nint.Zero;
|
||||
|
||||
public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -20,5 +20,7 @@ namespace Ryujinx.HLE.HOS.SystemState
|
||||
SimplifiedChinese,
|
||||
TraditionalChinese,
|
||||
BrazilianPortuguese,
|
||||
Polish,
|
||||
Thai,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -18,5 +18,7 @@ namespace Ryujinx.HLE.HOS.SystemState
|
||||
TraditionalChinese,
|
||||
SimplifiedChinese,
|
||||
BrazilianPortuguese,
|
||||
Polish,
|
||||
Thai,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc
|
||||
[CmifCommand(10100)] // 1.0.0-5.1.0
|
||||
[CmifCommand(10102)] // 6.0.0-9.2.0
|
||||
[CmifCommand(10104)] // 10.0.0+
|
||||
public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
public Result SaveReportOld([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
ProcessPlayReport(PlayReportKind.Normal, gameRoomBuffer, reportBuffer, pid, Uid.Null);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(10106)] // 21.0.0+
|
||||
public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid, bool optInCheckEnabled)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
@@ -48,7 +61,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc
|
||||
[CmifCommand(10101)] // 1.0.0-5.1.0
|
||||
[CmifCommand(10103)] // 6.0.0-9.2.0
|
||||
[CmifCommand(10105)] // 10.0.0+
|
||||
public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
public Result SaveReportWithUserOld(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
ProcessPlayReport(PlayReportKind.Normal, gameRoomBuffer, reportBuffer, pid, userId, true);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(10107)] // 21.0.0+
|
||||
public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid, bool optInCheckEnabled)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ namespace Ryujinx.Horizon.Sdk.Prepo
|
||||
{
|
||||
interface IPrepoService : IServiceObject
|
||||
{
|
||||
Result SaveReport(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveReportWithUser(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveReportOld(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveReport(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid, bool optInCheckEnabled);
|
||||
Result SaveReportWithUserOld(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveReportWithUser(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid, bool optInCheckEnabled);
|
||||
Result RequestImmediateTransmission();
|
||||
Result GetTransmissionStatus(out int status);
|
||||
Result GetSystemSessionId(out ulong systemSessionId);
|
||||
|
||||
@@ -9,10 +9,20 @@ using static SDL.SDL3;
|
||||
|
||||
namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
|
||||
|
||||
public unsafe class SDL3GamepadDriver : IGamepadDriver
|
||||
{
|
||||
private readonly Dictionary<SDL_JoystickID, string> _gamepadsInstanceIdsMapping;
|
||||
private readonly Dictionary<SDL_JoystickID, string> _gamepadsIds;
|
||||
/// <summary>
|
||||
/// Unlinked joy-cons
|
||||
/// </summary>
|
||||
private readonly Dictionary<SDL_JoystickID, string> _joyConsIds;
|
||||
/// <summary>
|
||||
/// Linked joy-cons, remove dual joy-con from <c>_gamepadsIds</c> when a linked joy-con is removed
|
||||
/// </summary>
|
||||
private readonly Dictionary<SDL_JoystickID,string> _linkedJoyConsIds;
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
public ReadOnlySpan<string> GamepadsIds
|
||||
@@ -21,7 +31,11 @@ namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _gamepadsIds.Values.ToArray();
|
||||
List<string> temp = [];
|
||||
temp.AddRange(_gamepadsIds.Values);
|
||||
temp.AddRange(_joyConsIds.Values);
|
||||
temp.AddRange(_linkedJoyConsIds.Values);
|
||||
return temp.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +49,8 @@ namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
_gamepadsInstanceIdsMapping = new Dictionary<SDL_JoystickID, string>();
|
||||
_gamepadsIds = [];
|
||||
_joyConsIds = [];
|
||||
_linkedJoyConsIds = [];
|
||||
|
||||
SDL3Driver.Instance.Initialize();
|
||||
SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
|
||||
@@ -92,7 +108,7 @@ namespace Ryujinx.Input.SDL3
|
||||
int guidIndex = 0;
|
||||
id = guidIndex + "-" + guidString;
|
||||
|
||||
while (_gamepadsIds.ContainsValue(id))
|
||||
while (_gamepadsIds.ContainsValue(id) || _joyConsIds.ContainsValue(id) || _linkedJoyConsIds.ContainsValue(id))
|
||||
{
|
||||
id = (++guidIndex) + "-" + guidString;
|
||||
}
|
||||
@@ -104,16 +120,47 @@ namespace Ryujinx.Input.SDL3
|
||||
private void HandleJoyStickDisconnected(SDL_JoystickID joystickInstanceId)
|
||||
{
|
||||
bool joyConPairDisconnected = false;
|
||||
string fakeId = null;
|
||||
|
||||
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_gamepadsIds.Remove(joystickInstanceId);
|
||||
if (!SDL3JoyConPair.IsCombinable(_gamepadsIds))
|
||||
if (!_linkedJoyConsIds.ContainsKey(joystickInstanceId))
|
||||
{
|
||||
_gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id));
|
||||
if (!_joyConsIds.Remove(joystickInstanceId))
|
||||
{
|
||||
_gamepadsIds.Remove(joystickInstanceId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (string matchId in _gamepadsIds.Values)
|
||||
{
|
||||
if (matchId.Contains(id))
|
||||
{
|
||||
fakeId = matchId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string leftId = fakeId!.Split('_')[0];
|
||||
string rightId = fakeId!.Split('_')[1];
|
||||
|
||||
if (leftId == id)
|
||||
{
|
||||
_linkedJoyConsIds.Remove(GetInstanceIdFromId(rightId));
|
||||
_joyConsIds.Add(GetInstanceIdFromId(rightId), rightId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_linkedJoyConsIds.Remove(GetInstanceIdFromId(leftId));
|
||||
_joyConsIds.Add(GetInstanceIdFromId(leftId), leftId);
|
||||
}
|
||||
|
||||
_linkedJoyConsIds.Remove(joystickInstanceId);
|
||||
_gamepadsIds.Remove(GetInstanceIdFromId(fakeId));
|
||||
joyConPairDisconnected = true;
|
||||
}
|
||||
}
|
||||
@@ -121,13 +168,14 @@ namespace Ryujinx.Input.SDL3
|
||||
OnGamepadDisconnected?.Invoke(id);
|
||||
if (joyConPairDisconnected)
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id);
|
||||
OnGamepadDisconnected?.Invoke(fakeId);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleJoyStickConnected(SDL_JoystickID joystickInstanceId)
|
||||
{
|
||||
bool joyConPairConnected = false;
|
||||
string fakeId = null;
|
||||
|
||||
if (SDL_IsGamepad(joystickInstanceId))
|
||||
{
|
||||
@@ -149,27 +197,40 @@ namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
_gamepadsIds.Add(joystickInstanceId, id);
|
||||
|
||||
if (SDL3JoyConPair.IsCombinable(_gamepadsIds))
|
||||
if (!SDL3JoyCon.IsJoyCon(joystickInstanceId))
|
||||
{
|
||||
// TODO - It appears that you can only have one joy con pair connected at a time?
|
||||
// This was also the behavior before SDL3
|
||||
_gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id));
|
||||
uint fakeInstanceID = uint.MaxValue;
|
||||
while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceID, SDL3JoyConPair.Id))
|
||||
_gamepadsIds.Add(joystickInstanceId, id);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SDL3JoyConPair.IsCombinable(joystickInstanceId, _joyConsIds, out SDL_JoystickID match))
|
||||
{
|
||||
fakeInstanceID--;
|
||||
_joyConsIds.Remove(match, out string matchId);
|
||||
_linkedJoyConsIds.Add(joystickInstanceId, id);
|
||||
_linkedJoyConsIds.Add(match, matchId);
|
||||
|
||||
uint fakeInstanceId = uint.MaxValue;
|
||||
fakeId = SDL3JoyCon.IsLeftJoyCon(joystickInstanceId)
|
||||
? $"{id}_{matchId}"
|
||||
: $"{matchId}_{id}";
|
||||
while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceId, fakeId))
|
||||
{
|
||||
fakeInstanceId--;
|
||||
}
|
||||
_gamepadsInstanceIdsMapping.Add((SDL_JoystickID)fakeInstanceId, fakeId);
|
||||
joyConPairConnected = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_joyConsIds.Add(joystickInstanceId, id);
|
||||
}
|
||||
joyConPairConnected = true;
|
||||
}
|
||||
}
|
||||
|
||||
OnGamepadConnected?.Invoke(id);
|
||||
if (joyConPairConnected)
|
||||
{
|
||||
OnGamepadConnected?.Invoke(SDL3JoyConPair.Id);
|
||||
OnGamepadConnected?.Invoke(fakeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,10 +254,22 @@ namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(gamepad.Value);
|
||||
}
|
||||
|
||||
foreach (var gamepad in _joyConsIds)
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(gamepad.Value);
|
||||
}
|
||||
|
||||
foreach (var gamepad in _linkedJoyConsIds)
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(gamepad.Value);
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_gamepadsIds.Clear();
|
||||
_joyConsIds.Clear();
|
||||
_linkedJoyConsIds.Clear();
|
||||
}
|
||||
|
||||
SDL3Driver.Instance.Dispose();
|
||||
@@ -215,11 +288,27 @@ namespace Ryujinx.Input.SDL3
|
||||
|
||||
public IGamepad GetGamepad(string id)
|
||||
{
|
||||
if (id == SDL3JoyConPair.Id)
|
||||
// joy-con pair ids is the combined ids of its parts which are split using a '_'
|
||||
if (id.Contains('_'))
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return SDL3JoyConPair.GetGamepad(_gamepadsIds);
|
||||
string leftId = id.Split('_')[0];
|
||||
string rightId = id.Split('_')[1];
|
||||
|
||||
SDL_JoystickID leftInstanceId = GetInstanceIdFromId(leftId);
|
||||
SDL_JoystickID rightInstanceId = GetInstanceIdFromId(rightId);
|
||||
|
||||
SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad(leftInstanceId);
|
||||
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad(rightInstanceId);
|
||||
|
||||
if (leftGamepadHandle == null || rightGamepadHandle == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, leftId),
|
||||
new SDL3JoyCon(rightGamepadHandle, rightId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +321,7 @@ namespace Ryujinx.Input.SDL3
|
||||
return null;
|
||||
}
|
||||
|
||||
if (SDL_GetGamepadName(gamepadHandle).StartsWith(SDL3JoyCon.Prefix))
|
||||
if (SDL3JoyCon.IsJoyCon(instanceId))
|
||||
{
|
||||
return new SDL3JoyCon(gamepadHandle, id);
|
||||
}
|
||||
@@ -249,6 +338,22 @@ namespace Ryujinx.Input.SDL3
|
||||
yield return GetGamepad(gamepad.Value);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_joyConsIds)
|
||||
{
|
||||
foreach (var gamepad in _joyConsIds)
|
||||
{
|
||||
yield return GetGamepad(gamepad.Value);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_linkedJoyConsIds)
|
||||
{
|
||||
foreach (var gamepad in _linkedJoyConsIds)
|
||||
{
|
||||
yield return GetGamepad(gamepad.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ namespace Ryujinx.Input.SDL3
|
||||
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _leftButtonsDriverMapping = new()
|
||||
{
|
||||
{GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
|
||||
{GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
|
||||
{GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
|
||||
{GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
|
||||
{GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{GamepadButtonInputId.Minus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START},
|
||||
{GamepadButtonInputId.LeftShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
{GamepadButtonInputId.LeftTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
@@ -37,10 +37,10 @@ namespace Ryujinx.Input.SDL3
|
||||
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _rightButtonsDriverMapping = new()
|
||||
{
|
||||
{GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
|
||||
{GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
|
||||
{GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
|
||||
{GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
|
||||
{GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{GamepadButtonInputId.Plus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START},
|
||||
{GamepadButtonInputId.RightShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
{GamepadButtonInputId.RightTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
@@ -398,5 +398,15 @@ namespace Ryujinx.Input.SDL3
|
||||
|
||||
return SDL_GetGamepadButton(_gamepadHandle, button);
|
||||
}
|
||||
|
||||
public static bool IsJoyCon(SDL_JoystickID gamepadsId)
|
||||
{
|
||||
return SDL_GetGamepadNameForID(gamepadsId) is LeftName or RightName;
|
||||
}
|
||||
|
||||
public static bool IsLeftJoyCon(SDL_JoystickID gamepadsId)
|
||||
{
|
||||
return SDL_GetGamepadNameForID(gamepadsId) is LeftName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Ryujinx.Input.SDL3
|
||||
public const string Id = "JoyConPair";
|
||||
string IGamepad.Id => Id;
|
||||
|
||||
public string Name => "* Nintendo Switch Joy-Con (L/R)";
|
||||
public string Name => "Nintendo Switch Dual Joy-Con (L/R)";
|
||||
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
|
||||
|
||||
public void Dispose()
|
||||
@@ -96,44 +96,23 @@ namespace Ryujinx.Input.SDL3
|
||||
right.SetTriggerThreshold(triggerThreshold);
|
||||
}
|
||||
|
||||
public static bool IsCombinable(Dictionary<SDL_JoystickID, string> gamepadsIds)
|
||||
public static bool IsCombinable(SDL_JoystickID joyCon1, Dictionary<SDL_JoystickID, string> joyConIds, out SDL_JoystickID match)
|
||||
{
|
||||
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
|
||||
return leftIndex >= 0 && rightIndex >= 0;
|
||||
}
|
||||
bool isLeft = SDL3JoyCon.IsLeftJoyCon(joyCon1);
|
||||
string matchName = isLeft ? SDL3JoyCon.RightName : SDL3JoyCon.LeftName;
|
||||
match = 0;
|
||||
|
||||
private static (int leftIndex, int rightIndex) DetectJoyConPair(Dictionary<SDL_JoystickID, string> gamepadsIds)
|
||||
{
|
||||
Dictionary<string, SDL_JoystickID> gamepadNames = gamepadsIds
|
||||
.Where(gamepadId => gamepadId.Value != Id && SDL_GetGamepadNameForID(gamepadId.Key) is SDL3JoyCon.LeftName or SDL3JoyCon.RightName)
|
||||
.Select(gamepad => (SDL_GetGamepadNameForID(gamepad.Key), gamepad.Key))
|
||||
.ToDictionary();
|
||||
SDL_JoystickID idx;
|
||||
int leftIndex = gamepadNames.TryGetValue(SDL3JoyCon.LeftName, out idx) ? (int)idx : -1;
|
||||
int rightIndex = gamepadNames.TryGetValue(SDL3JoyCon.RightName, out idx) ? (int)idx : -1;
|
||||
|
||||
return (leftIndex, rightIndex);
|
||||
}
|
||||
|
||||
public unsafe static IGamepad GetGamepad(Dictionary<SDL_JoystickID, string> gamepadsIds)
|
||||
{
|
||||
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
|
||||
|
||||
if (leftIndex <= 0 || rightIndex <= 0)
|
||||
foreach (var joyConId in joyConIds.Keys)
|
||||
{
|
||||
return null;
|
||||
if (SDL_GetGamepadNameForID(joyConId) == matchName)
|
||||
{
|
||||
match = joyConId;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)leftIndex);
|
||||
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)rightIndex);
|
||||
|
||||
if (leftGamepadHandle == null || rightGamepadHandle == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, gamepadsIds[(SDL_JoystickID)leftIndex]),
|
||||
new SDL3JoyCon(rightGamepadHandle, gamepadsIds[(SDL_JoystickID)rightIndex]));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ namespace Ryujinx.Memory.WindowsShared
|
||||
{
|
||||
SplitForMap((ulong)location, (ulong)size, srcOffset);
|
||||
|
||||
IntPtr ptr = WindowsApi.MapViewOfFile3(
|
||||
nint ptr = WindowsApi.MapViewOfFile3(
|
||||
sharedMemory,
|
||||
WindowsApi.CurrentProcessHandle,
|
||||
location,
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace Ryujinx.Tests.Memory
|
||||
|
||||
// Create some info to be used for managing the native writing loop.
|
||||
int stateSize = Unsafe.SizeOf<NativeWriteLoopState>();
|
||||
IntPtr statePtr = Marshal.AllocHGlobal(stateSize);
|
||||
nint statePtr = Marshal.AllocHGlobal(stateSize);
|
||||
Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize);
|
||||
|
||||
ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef<NativeWriteLoopState>((void*)statePtr);
|
||||
|
||||
@@ -190,7 +190,7 @@ namespace Ryujinx.Ava.Common.Locale
|
||||
|
||||
}
|
||||
|
||||
foreach (LocalesJson file in _localeData.Value.LocalesFiles.Values)
|
||||
foreach ((string fileName, LocalesJson file) in _localeData.Value.LocalesFiles)
|
||||
{
|
||||
foreach (LocalesEntry locale in file.Locales)
|
||||
{
|
||||
@@ -206,7 +206,7 @@ namespace Ryujinx.Ava.Common.Locale
|
||||
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey))
|
||||
if (!Enum.TryParse<LocaleKeys>(fileName == "Root.json" ? locale.ID : $"{fileName[..^".json".Length]}_{locale.ID}" , out LocaleKeys localeKey))
|
||||
continue;
|
||||
|
||||
string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val)
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace Ryujinx.Ava.Input
|
||||
AvaKey.OemComma,
|
||||
AvaKey.OemPeriod,
|
||||
AvaKey.OemQuestion,
|
||||
AvaKey.OemBackslash,
|
||||
AvaKey.OemPipe,
|
||||
|
||||
// NOTE: invalid
|
||||
AvaKey.None
|
||||
|
||||
@@ -18,6 +18,7 @@ using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.SystemInterop;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Graphics.RenderDocApi;
|
||||
using Ryujinx.Graphics.Vulkan.MoltenVK;
|
||||
using Ryujinx.Headless;
|
||||
using Ryujinx.SDL3.Common;
|
||||
@@ -41,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;
|
||||
|
||||
@@ -80,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.
|
||||
|
||||
@@ -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" />
|
||||
@@ -77,16 +72,18 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.Apple\Ryujinx.Audio.Backends.Apple.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
|
||||
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Avalonia.Threading;
|
||||
using DiscordRPC;
|
||||
using LibHac.Common;
|
||||
using LibHac.Ns;
|
||||
using Ryujinx.Audio.Backends.Apple;
|
||||
using Ryujinx.Audio.Backends.Dummy;
|
||||
using Ryujinx.Audio.Backends.OpenAL;
|
||||
using Ryujinx.Audio.Backends.SDL3;
|
||||
@@ -475,10 +476,10 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
TouchScreenManager.Initialize(Device);
|
||||
|
||||
_viewModel.IsGameRunning = true;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
_viewModel.IsGameRunning = true;
|
||||
_viewModel.IsPaused = false;
|
||||
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI);
|
||||
});
|
||||
|
||||
@@ -577,6 +578,7 @@ namespace Ryujinx.Ava.Systems
|
||||
public void Stop()
|
||||
{
|
||||
_isActive = false;
|
||||
_viewModel.IsPaused = false;
|
||||
_playTimer.Stop();
|
||||
}
|
||||
|
||||
@@ -703,8 +705,8 @@ namespace Ryujinx.Ava.Systems
|
||||
if (userError is UserError.NoFirmware)
|
||||
{
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance[LocaleKeys.Dialog_Firmware_InstallerNotInstalledMessage],
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.Dialog_Firmware_InstallerEmbeddedMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
string.Empty);
|
||||
@@ -734,8 +736,8 @@ namespace Ryujinx.Ava.Systems
|
||||
_viewModel.RefreshFirmwareStatus();
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstalledMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.Dialog_Firmware_InstallerInstalledMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.Dialog_Firmware_InstallerEmbeddedMessageSuccess, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||
string.Empty,
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
||||
@@ -949,6 +951,9 @@ namespace Ryujinx.Ava.Systems
|
||||
AudioBackend.Dummy
|
||||
];
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
availableBackends.Insert(0, AudioBackend.AudioToolbox);
|
||||
|
||||
AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value;
|
||||
|
||||
if (preferredBackend is AudioBackend.SDL2)
|
||||
@@ -985,6 +990,9 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
deviceDriver = currentBackend switch
|
||||
{
|
||||
#pragma warning disable CA1416 // Platform compatibility is enforced in AppleHardwareDeviceDriver.IsSupported, before any potentially platform-sensitive code can run.
|
||||
AudioBackend.AudioToolbox => InitializeAudioBackend<AppleHardwareDeviceDriver>(AudioBackend.AudioToolbox, nextBackend),
|
||||
#pragma warning restore CA1416
|
||||
AudioBackend.SDL3 => InitializeAudioBackend<SDL3HardwareDeviceDriver>(AudioBackend.SDL3, nextBackend),
|
||||
AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend),
|
||||
AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend),
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
OpenAl,
|
||||
SoundIo,
|
||||
SDL3,
|
||||
AudioToolbox,
|
||||
SDL2 = SDL3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace Ryujinx.Ava.Systems.Configuration.System
|
||||
SimplifiedChinese,
|
||||
TraditionalChinese,
|
||||
BrazilianPortuguese,
|
||||
Polish,
|
||||
Thai,
|
||||
}
|
||||
|
||||
public static class LanguageEnumHelper
|
||||
|
||||
@@ -7,6 +7,7 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Systems.Update.Client;
|
||||
using Ryujinx.Systems.Update.Common;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -46,6 +47,12 @@ namespace Ryujinx.Ava.Systems
|
||||
return Return<VersionResponse>.Failure(
|
||||
new MessageError("DNS resolution error occurred. Is your internet down?"));
|
||||
}
|
||||
catch (HttpRequestException hre)
|
||||
when (hre.StatusCode is HttpStatusCode.BadGateway)
|
||||
{
|
||||
return Return<VersionResponse>.Failure(
|
||||
new MessageError("Could not connect to the update server, but it appears like you have internet. It seems like the update server is offline, try again later."));
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)
|
||||
|
||||
@@ -14,9 +14,9 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
private static string GetErrorTitle(UserError error) =>
|
||||
error switch
|
||||
{
|
||||
UserError.NoKeys => LocaleManager.Instance[LocaleKeys.UserErrorNoKeys],
|
||||
UserError.NoFirmware => LocaleManager.Instance[LocaleKeys.UserErrorNoFirmware],
|
||||
UserError.FirmwareParsingFailed => LocaleManager.Instance[LocaleKeys.UserErrorFirmwareParsingFailed],
|
||||
UserError.NoKeys => LocaleManager.Instance[LocaleKeys.Error_NoKeysFound],
|
||||
UserError.NoFirmware => LocaleManager.Instance[LocaleKeys.Error_NoFirmwareFound],
|
||||
UserError.FirmwareParsingFailed => LocaleManager.Instance[LocaleKeys.Error_FirmwareParsingFailed],
|
||||
UserError.ApplicationNotFound => LocaleManager.Instance[LocaleKeys.UserErrorApplicationNotFound],
|
||||
UserError.Unknown => LocaleManager.Instance[LocaleKeys.UserErrorUnknown],
|
||||
_ => LocaleManager.Instance[LocaleKeys.UserErrorUndefined],
|
||||
@@ -25,9 +25,9 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
private static string GetErrorDescription(UserError error) =>
|
||||
error switch
|
||||
{
|
||||
UserError.NoKeys => LocaleManager.Instance[LocaleKeys.UserErrorNoKeysDescription],
|
||||
UserError.NoFirmware => LocaleManager.Instance[LocaleKeys.UserErrorNoFirmwareDescription],
|
||||
UserError.FirmwareParsingFailed => LocaleManager.Instance[LocaleKeys.UserErrorFirmwareParsingFailedDescription],
|
||||
UserError.NoKeys => LocaleManager.Instance[LocaleKeys.Error_NoKeysFoundDescription],
|
||||
UserError.NoFirmware => LocaleManager.Instance[LocaleKeys.Error_NoFirmwareFoundDescription],
|
||||
UserError.FirmwareParsingFailed => LocaleManager.Instance[LocaleKeys.Error_FirmwareParsingFailedDescription],
|
||||
UserError.ApplicationNotFound => LocaleManager.Instance[LocaleKeys.UserErrorApplicationNotFoundDescription],
|
||||
UserError.Unknown => LocaleManager.Instance[LocaleKeys.UserErrorUnknownDescription],
|
||||
_ => LocaleManager.Instance[LocaleKeys.UserErrorUndefinedDescription],
|
||||
|
||||
@@ -2,8 +2,12 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform;
|
||||
using Ryujinx.Ava.Systems.Configuration;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.RenderDocApi;
|
||||
using Ryujinx.HLE;
|
||||
using SPB.Graphics;
|
||||
using SPB.Platform;
|
||||
using SPB.Platform.GLX;
|
||||
@@ -30,6 +34,7 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
protected nint MetalLayer { get; set; }
|
||||
|
||||
public delegate void UpdateBoundsCallbackDelegate(Rect rect);
|
||||
|
||||
private UpdateBoundsCallbackDelegate _updateBoundsCallback;
|
||||
|
||||
public event EventHandler<nint> WindowCreated;
|
||||
@@ -46,6 +51,55 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
|
||||
protected virtual void OnWindowDestroyed() { }
|
||||
|
||||
public bool ToggleRenderDocCapture(Switch device)
|
||||
{
|
||||
if (!RenderDoc.IsAvailable) return false;
|
||||
|
||||
if (RenderDoc.IsFrameCapturing)
|
||||
{
|
||||
if (EndRenderDocCapture())
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (StartRenderDocCapture(device))
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool StartRenderDocCapture(Switch device)
|
||||
{
|
||||
if (!RenderDoc.IsAvailable) return false;
|
||||
|
||||
if (RenderDoc.IsFrameCapturing) return false;
|
||||
|
||||
RenderDoc.StartFrameCapture(nint.Zero, WindowHandle);
|
||||
RenderDoc.SetCaptureTitle(TitleHelper.FormatRenderDocCaptureTitle(device.Processes.ActiveApplication, Program.Version));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool EndRenderDocCapture()
|
||||
{
|
||||
if (!RenderDoc.IsAvailable) return false;
|
||||
if (!RenderDoc.IsFrameCapturing) return false;
|
||||
|
||||
return RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle);
|
||||
}
|
||||
|
||||
public bool DiscardRenderDocCapture()
|
||||
{
|
||||
if (!RenderDoc.IsAvailable) return false;
|
||||
if (!RenderDoc.IsFrameCapturing) return false;
|
||||
|
||||
return RenderDoc.IsFrameCapturing && RenderDoc.DiscardFrameCapture(nint.Zero, WindowHandle);
|
||||
}
|
||||
|
||||
protected virtual void OnWindowDestroying()
|
||||
{
|
||||
WindowHandle = nint.Zero;
|
||||
@@ -124,7 +178,9 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
}
|
||||
else
|
||||
{
|
||||
X11Window = PlatformHelper.CreateOpenGLWindow(new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100, 100) as GLXWindow;
|
||||
X11Window = PlatformHelper.CreateOpenGLWindow(
|
||||
new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100,
|
||||
100) as GLXWindow;
|
||||
}
|
||||
|
||||
WindowHandle = X11Window.WindowHandle.RawHandle;
|
||||
@@ -138,7 +194,7 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
{
|
||||
_className = "NativeWindow-" + Guid.NewGuid();
|
||||
|
||||
_wndProcDelegate = delegate (nint hWnd, WindowsMessages msg, nint wParam, nint lParam)
|
||||
_wndProcDelegate = delegate(nint hWnd, WindowsMessages msg, nint wParam, nint lParam)
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
@@ -161,7 +217,8 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
|
||||
RegisterClassEx(ref wndClassEx);
|
||||
|
||||
WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480, control.Handle, nint.Zero, nint.Zero, nint.Zero);
|
||||
WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480,
|
||||
control.Handle, nint.Zero, nint.Zero, nint.Zero);
|
||||
|
||||
SetWindowLongPtrW(control.Handle, GWLP_WNDPROC, wndClassEx.lpfnWndProc);
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -15,6 +16,7 @@ using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.RenderDocApi;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -52,6 +54,9 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
Name = FormatTitle();
|
||||
|
||||
// Disable menu animations
|
||||
FAUISettings.SetAnimationsEnabledAtAppLevel(false);
|
||||
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
|
||||
@@ -184,7 +184,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
_controller = 0;
|
||||
}
|
||||
|
||||
if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1)
|
||||
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
|
||||
{
|
||||
ControllerType controller = Controllers[_controller].Type;
|
||||
|
||||
@@ -467,7 +467,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
IsModified = true;
|
||||
RevertChanges();
|
||||
FindPairedDeviceInConfigFile();
|
||||
|
||||
|
||||
_isChangeTrackingActive = true; // Enable configuration change tracking
|
||||
|
||||
}
|
||||
@@ -521,7 +521,17 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1)
|
||||
{
|
||||
Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
|
||||
int controllerIndex = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
|
||||
|
||||
// Avalonia bug: setting a newly instanced ComboBox to 0
|
||||
// causes the selected item to show up blank
|
||||
// Workaround: set the box to 1 and then 0
|
||||
if (controllerIndex == 0)
|
||||
{
|
||||
Controller = 1;
|
||||
}
|
||||
|
||||
Controller = controllerIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -576,7 +586,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
DeviceList.Clear();
|
||||
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
|
||||
|
||||
int controllerNumber = 0;
|
||||
|
||||
foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds)
|
||||
{
|
||||
using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
|
||||
@@ -593,6 +603,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
if (gamepad != null)
|
||||
{
|
||||
int controllerNumber = 0;
|
||||
string name = GetUniqueGamepadName(gamepad, ref controllerNumber);
|
||||
Devices.Add((DeviceType.Controller, id, name));
|
||||
}
|
||||
@@ -950,8 +961,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad
|
||||
Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId);
|
||||
|
||||
LoadDevice();
|
||||
_isLoaded = false;
|
||||
LoadConfiguration();
|
||||
LoadDevice();
|
||||
_isLoaded = true;
|
||||
|
||||
OnPropertyChanged();
|
||||
IsModified = false;
|
||||
|
||||
@@ -38,6 +38,7 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.UI;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Cpu;
|
||||
using Ryujinx.Graphics.RenderDocApi;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
@@ -104,7 +105,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
[ObservableProperty] public partial Brush ProgressBarForegroundColor { get; set; }
|
||||
|
||||
[ObservableProperty] public partial Brush ProgressBarBackgroundColor { get; set; }
|
||||
|
||||
|
||||
#pragma warning disable MVVMTK0042 // Must stay a normal observable field declaration since this is used as an out parameter target
|
||||
[ObservableProperty] private ReadOnlyObservableCollection<ApplicationData> _appsObservableList;
|
||||
#pragma warning restore MVVMTK0042
|
||||
@@ -129,8 +130,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
[ObservableProperty] public partial string LastScannedAmiiboId { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64;
|
||||
[ObservableProperty] public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64;
|
||||
[ObservableProperty] public partial bool ShowContent { get; set; } = true;
|
||||
|
||||
[ObservableProperty] public partial float VolumeBeforeMute { get; set; }
|
||||
@@ -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;
|
||||
@@ -907,25 +941,25 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
await ContentDialogHelper.CreateErrorDialog(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.DialogFirmwareInstallerFirmwareNotFoundErrorMessage, filename));
|
||||
LocaleKeys.Dialog_Firmware_InstallerFirmwareNotFound, filename));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
string dialogTitle = LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.DialogFirmwareInstallerFirmwareInstallTitle, firmwareVersion.VersionString);
|
||||
LocaleKeys.Dialog_Firmware_InstallerTitle, firmwareVersion.VersionString);
|
||||
string dialogMessage = LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.DialogFirmwareInstallerFirmwareInstallMessage, firmwareVersion.VersionString);
|
||||
LocaleKeys.Dialog_Firmware_InstallerMainMessage, firmwareVersion.VersionString);
|
||||
|
||||
SystemVersion currentVersion = ContentManager.GetCurrentFirmwareVersion();
|
||||
if (currentVersion != null)
|
||||
{
|
||||
dialogMessage += LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.DialogFirmwareInstallerFirmwareInstallSubMessage, currentVersion.VersionString);
|
||||
LocaleKeys.Dialog_Firmware_InstallerSubMessage, currentVersion.VersionString);
|
||||
}
|
||||
|
||||
dialogMessage +=
|
||||
LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallConfirmMessage];
|
||||
LocaleManager.Instance[LocaleKeys.Dialog_Firmware_InstallerConfirmMessage];
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
dialogTitle,
|
||||
@@ -935,7 +969,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||
|
||||
UpdateWaitWindow waitingDialog = new(dialogTitle,
|
||||
LocaleManager.Instance[LocaleKeys.DialogFirmwareInstallerFirmwareInstallWaitMessage]);
|
||||
LocaleManager.Instance[LocaleKeys.Dialog_Firmware_InstallerWaitMessage]);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
@@ -957,7 +991,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
waitingDialog.Close();
|
||||
|
||||
string message = LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.DialogFirmwareInstallerFirmwareInstallSuccessMessage,
|
||||
LocaleKeys.Dialog_Firmware_InstallerSuccessMessage,
|
||||
firmwareVersion.VersionString);
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
@@ -1025,18 +1059,18 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
|
||||
string dialogTitle =
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallTitle);
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.MenuBar_Actions_InstallKeysLabel);
|
||||
string dialogMessage =
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage);
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.Dialog_Keys_InstallerMainMessage);
|
||||
|
||||
if (ContentManager.AreKeysAlredyPresent(systemDirectory))
|
||||
if (ContentManager.AreKeysAlreadyPresent(systemDirectory))
|
||||
{
|
||||
dialogMessage +=
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys
|
||||
.DialogKeysInstallerKeysInstallSubMessage);
|
||||
.Dialog_Keys_InstallerSubMessage);
|
||||
}
|
||||
|
||||
dialogMessage += LocaleManager.Instance[LocaleKeys.DialogKeysInstallerKeysInstallConfirmMessage];
|
||||
dialogMessage += LocaleManager.Instance[LocaleKeys.Dialog_Keys_InstallerConfirmInstall];
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
dialogTitle,
|
||||
@@ -1046,7 +1080,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
||||
|
||||
UpdateWaitWindow waitingDialog = new(dialogTitle,
|
||||
LocaleManager.Instance[LocaleKeys.DialogKeysInstallerKeysInstallWaitMessage]);
|
||||
LocaleManager.Instance[LocaleKeys.Dialog_Keys_InstallerWaitMessage]);
|
||||
|
||||
if (result == UserResult.Yes)
|
||||
{
|
||||
@@ -1069,7 +1103,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
string message =
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys
|
||||
.DialogKeysInstallerKeysInstallSuccessMessage);
|
||||
.Dialog_Keys_InstallerSuccessMessage);
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
dialogTitle,
|
||||
@@ -1091,7 +1125,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
if (ex is FormatException)
|
||||
{
|
||||
message = LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.DialogKeysInstallerKeysNotFoundErrorMessage, filename);
|
||||
LocaleKeys.Dialog_Keys_InstallerKeysNotFound, filename);
|
||||
}
|
||||
|
||||
await ContentDialogHelper.CreateErrorDialog(message);
|
||||
@@ -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)
|
||||
@@ -1357,9 +1414,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.Dialog_Firmware_InstallFromFileDialogTitle],
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.FileDialogAllTypes])
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.xci", "*.zip"],
|
||||
AppleUniformTypeIdentifiers = ["com.ryujinx.xci", "public.zip-archive"],
|
||||
@@ -1388,7 +1446,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public async Task InstallFirmwareFromFolder()
|
||||
{
|
||||
Optional<IStorageFolder> result = await StorageProvider.OpenSingleFolderPickerAsync();
|
||||
Optional<IStorageFolder> result = await StorageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.Dialog_Firmware_InstallFromFolderDialogTitle]
|
||||
});
|
||||
|
||||
if (result.HasValue)
|
||||
{
|
||||
@@ -1400,6 +1461,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.Dialog_Keys_InstallFromFileDialogTitle],
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new("KEYS")
|
||||
@@ -1419,7 +1481,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public async Task InstallKeysFromFolder()
|
||||
{
|
||||
Optional<IStorageFolder> result = await StorageProvider.OpenSingleFolderPickerAsync();
|
||||
Optional<IStorageFolder> result = await StorageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.Dialog_Keys_InstallFromFolderDialogTitle]
|
||||
});
|
||||
|
||||
if (result.HasValue)
|
||||
{
|
||||
@@ -1773,14 +1838,13 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
if (version != null)
|
||||
{
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarSystemVersion,
|
||||
version.VersionString);
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBar_FirmwareVersion, version.VersionString);
|
||||
|
||||
hasApplet = version.Major > 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBarSystemVersion, "NaN");
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.StatusBar_FirmwareVersion, "NaN");
|
||||
}
|
||||
|
||||
IsAppletMenuActive = hasApplet;
|
||||
@@ -1864,6 +1928,69 @@ 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()
|
||||
{
|
||||
RenderDoc.ReloadApi(ignoreAlreadyLoaded: true);
|
||||
|
||||
OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton), nameof(RenderDocIsAvailable));
|
||||
|
||||
if (RenderDoc.IsAvailable)
|
||||
RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
|
||||
NotificationHelper.ShowInformation(
|
||||
"RenderDoc API reloaded",
|
||||
RenderDoc.IsAvailable ? "RenderDoc is now available." : "RenderDoc is no longer available."
|
||||
);
|
||||
}
|
||||
|
||||
public void ToggleCapture()
|
||||
{
|
||||
if (ShowLoadProgress) return;
|
||||
|
||||
AppHost.RendererHost.EmbeddedWindow.ToggleRenderDocCapture(AppHost.Device);
|
||||
RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
}
|
||||
|
||||
public void ToggleFullscreen()
|
||||
{
|
||||
@@ -1955,7 +2082,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
if (ConfigurationState.Instance.Debug.EnableGdbStub)
|
||||
{
|
||||
NotificationHelper.ShowInformation(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, ConfigurationState.Instance.Debug.GdbStubPort.Value),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle,
|
||||
ConfigurationState.Instance.Debug.GdbStubPort.Value),
|
||||
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckGdbStubMessage]);
|
||||
}
|
||||
|
||||
@@ -1964,10 +2092,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
var memoryConfigurationLocaleKey = ConfigurationState.Instance.System.DramSize.Value switch
|
||||
{
|
||||
MemoryConfiguration.MemoryConfiguration4GiB or
|
||||
MemoryConfiguration.MemoryConfiguration4GiBAppletDev or
|
||||
MemoryConfiguration.MemoryConfiguration4GiBSystemDev => LocaleKeys.SettingsTabSystemDramSize4GiB,
|
||||
MemoryConfiguration.MemoryConfiguration4GiBAppletDev or
|
||||
MemoryConfiguration.MemoryConfiguration4GiBSystemDev =>
|
||||
LocaleKeys.SettingsTabSystemDramSize4GiB,
|
||||
MemoryConfiguration.MemoryConfiguration6GiB or
|
||||
MemoryConfiguration.MemoryConfiguration6GiBAppletDev => LocaleKeys.SettingsTabSystemDramSize6GiB,
|
||||
MemoryConfiguration.MemoryConfiguration6GiBAppletDev =>
|
||||
LocaleKeys.SettingsTabSystemDramSize6GiB,
|
||||
MemoryConfiguration.MemoryConfiguration8GiB => LocaleKeys.SettingsTabSystemDramSize8GiB,
|
||||
MemoryConfiguration.MemoryConfiguration12GiB => LocaleKeys.SettingsTabSystemDramSize12GiB,
|
||||
_ => LocaleKeys.SettingsTabSystemDramSize4GiB,
|
||||
@@ -1975,9 +2105,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
NotificationHelper.ShowWarning(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.NotificationLaunchCheckDramSizeTitle,
|
||||
LocaleKeys.NotificationLaunchCheckDramSizeTitle,
|
||||
LocaleManager.Instance[memoryConfigurationLocaleKey]
|
||||
),
|
||||
),
|
||||
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckDramSizeMessage]);
|
||||
}
|
||||
}
|
||||
@@ -2462,6 +2592,67 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
png.SaveTo(fileStream);
|
||||
});
|
||||
|
||||
public bool ShowStartCaptureButton => !RenderDocIsCapturing && RenderDoc.IsAvailable;
|
||||
public bool ShowEndCaptureButton => RenderDocIsCapturing && RenderDoc.IsAvailable;
|
||||
public static bool RenderDocIsAvailable => RenderDoc.IsAvailable;
|
||||
|
||||
public bool RenderDocIsCapturing
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton));
|
||||
}
|
||||
}
|
||||
|
||||
public static RelayCommand<MainWindowViewModel> StartRenderDocCapture { get; } =
|
||||
Commands.CreateConditional<MainWindowViewModel>(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress,
|
||||
viewModel =>
|
||||
{
|
||||
if (!RenderDoc.IsFrameCapturing)
|
||||
{
|
||||
if (viewModel.AppHost.RendererHost
|
||||
.EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device))
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture.");
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
});
|
||||
|
||||
public static RelayCommand<MainWindowViewModel> EndRenderDocCapture { get; } =
|
||||
Commands.CreateConditional<MainWindowViewModel>(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress,
|
||||
viewModel =>
|
||||
{
|
||||
if (RenderDoc.IsFrameCapturing)
|
||||
{
|
||||
if (viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture())
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture.");
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
});
|
||||
|
||||
public static RelayCommand<MainWindowViewModel> DiscardRenderDocCapture { get; } =
|
||||
Commands.CreateConditional<MainWindowViewModel>(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress,
|
||||
viewModel =>
|
||||
{
|
||||
if (RenderDoc.IsFrameCapturing)
|
||||
{
|
||||
if (viewModel.AppHost.RendererHost.EmbeddedWindow.DiscardRenderDocCapture())
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Discarded RenderDoc capture.");
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
});
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Audio.Backends.Apple;
|
||||
using Ryujinx.Audio.Backends.OpenAL;
|
||||
using Ryujinx.Audio.Backends.SDL3;
|
||||
using Ryujinx.Audio.Backends.SoundIo;
|
||||
@@ -277,6 +278,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
public bool IsOpenAlEnabled { get; set; }
|
||||
public bool IsSoundIoEnabled { get; set; }
|
||||
public bool IsSDL3Enabled { get; set; }
|
||||
public bool IsAudioToolboxEnabled { get; set; }
|
||||
public bool IsCustomResolutionScaleActive => _resolutionScale == 4;
|
||||
public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr;
|
||||
|
||||
@@ -524,12 +526,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported;
|
||||
IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported;
|
||||
IsSDL3Enabled = SDL3HardwareDeviceDriver.IsSupported;
|
||||
IsAudioToolboxEnabled = OperatingSystem.IsMacOS() && AppleHardwareDeviceDriver.IsSupported;
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(IsOpenAlEnabled));
|
||||
OnPropertyChanged(nameof(IsSoundIoEnabled));
|
||||
OnPropertyChanged(nameof(IsSDL3Enabled));
|
||||
OnPropertyChanged(nameof(IsAudioToolboxEnabled));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
xmlns:common="clr-namespace:Ryujinx.Common;assembly=Ryujinx.Common"
|
||||
xmlns:renderDocApi="clr-namespace:Ryujinx.Graphics.RenderDocApi;assembly=Ryujinx.Graphics.RenderDocApi"
|
||||
x:DataType="viewModels:MainWindowViewModel"
|
||||
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView">
|
||||
<Design.DataContext>
|
||||
@@ -62,6 +63,11 @@
|
||||
Command="{Binding OpenScreenshotsFolder}"
|
||||
Header="{ext:Locale MenuBarFileOpenScreenshotsFolder}"
|
||||
Icon="{ext:Icon fa-solid fa-image}" />
|
||||
<Separator IsVisible="{Binding ManageFileTypesVisible}" />
|
||||
<MenuItem Header="{ext:Locale MenuBar_File_ManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}" Icon="{ext:Icon fa-solid fa-clipboard}">
|
||||
<MenuItem Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBar_File_InstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" Icon="{ext:Icon fa-solid fa-square-plus}" />
|
||||
<MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBar_File_UninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" Icon="{ext:Icon fa-solid fa-square-minus}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Name="CloseRyujinxMenuItem"
|
||||
@@ -148,77 +154,120 @@
|
||||
IsVisible="{Binding !EnableNonGameRunningControls}">
|
||||
<MenuItem
|
||||
Name="PauseEmulationMenuItem"
|
||||
Header="{ext:Locale MenuBarOptionsPauseEmulation}"
|
||||
Header="{ext:Locale MenuBar_Actions_PauseEmulationButton}"
|
||||
Icon="{ext:Icon fa-solid fa-pause}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding !IsPaused}"
|
||||
IsVisible="{Binding !IsPaused}" />
|
||||
<MenuItem
|
||||
Name="ResumeEmulationMenuItem"
|
||||
Header="{ext:Locale MenuBarOptionsResumeEmulation}"
|
||||
Header="{ext:Locale MenuBar_Actions_ResumeEmulationButton}"
|
||||
Icon="{ext:Icon fa-solid fa-play}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding IsPaused}"
|
||||
IsVisible="{Binding IsPaused}" />
|
||||
<MenuItem
|
||||
Name="StopEmulationMenuItem"
|
||||
Header="{ext:Locale MenuBarOptionsStopEmulation}"
|
||||
Header="{ext:Locale MenuBar_Actions_StopEmulationButton}"
|
||||
Icon="{ext:Icon fa-solid fa-stop}"
|
||||
InputGesture="Escape"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{ext:Locale MenuBarOptionsSimulateWakeUpMessage}" Icon="{ext:Icon fa-solid fa-sun}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Name="RestartEmulationMenuItem"
|
||||
Header="{ext:Locale MenuBar_Actions_RestartEmulationButton}"
|
||||
Icon="{ext:Icon fa-solid fa-rotate-right}"
|
||||
InputGesture="Ctrl + R"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<Separator/>
|
||||
<MenuItem
|
||||
Command="{Binding OpenAmiiboWindow}"
|
||||
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
|
||||
Header="{ext:Locale MenuBarActionsScanAmiibo}"
|
||||
Icon="{ext:Icon fa-solid fa-cube}"
|
||||
Header="{ext:Locale MenuBar_Actions_ScanAmiiboButton}"
|
||||
Icon="{ext:Icon fa-solid fa-chess-rook}"
|
||||
InputGesture="Ctrl + A"
|
||||
IsEnabled="{Binding IsAmiiboRequested}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenBinFile}"
|
||||
AttachedToVisualTree="ScanBinAmiiboMenuItem_AttachedToVisualTree"
|
||||
Header="{ext:Locale MenuBarActionsScanAmiiboBin}"
|
||||
Icon="{ext:Icon fa-solid fa-cube}"
|
||||
Header="{ext:Locale MenuBar_Actions_ScanAmiiboFromBinButton}"
|
||||
Icon="{ext:Icon fa-solid fa-chess-rook}"
|
||||
IsVisible="{Binding CanScanAmiiboBinaries}"
|
||||
InputGesture="Ctrl + B"
|
||||
IsEnabled="{Binding IsAmiiboBinRequested}" />
|
||||
<Separator IsVisible="{Binding CanScanAmiiboBinaries}"/>
|
||||
<MenuItem
|
||||
Command="{Binding OpenSkylanderWindow}"
|
||||
AttachedToVisualTree="ScanSkylanderMenuItem_AttachedToVisualTree"
|
||||
Header="{ext:Locale MenuBar_Actions_ScanSkylanderButton}"
|
||||
Icon="{ext:Icon fa-solid fa-dragon}"
|
||||
IsVisible="{Binding ShowSkylanderActions}"
|
||||
InputGesture="Ctrl + S"
|
||||
IsEnabled="{Binding IsSkylanderRequested}" />
|
||||
<MenuItem
|
||||
Command="{Binding RemoveSkylander}"
|
||||
AttachedToVisualTree="RemoveSkylanderMenuItem_AttachedToVisualTree"
|
||||
Header="{ext:Locale MenuBar_Actions_RemoveSkylanderButton}"
|
||||
Icon="{ext:Icon fa-solid fa-dragon}"
|
||||
IsVisible="{Binding ShowSkylanderActions}"
|
||||
InputGesture="Ctrl + D"
|
||||
IsEnabled="{Binding HasSkylander}" />
|
||||
<Separator IsVisible="{Binding ShowSkylanderActions}"/>
|
||||
<MenuItem
|
||||
Command="{Binding SimulateWakeUpMessage}"
|
||||
Header="{ext:Locale MenuBarOptionsSimulateWakeUpMessage}"
|
||||
Icon="{ext:Icon fa-solid fa-sun}"
|
||||
InputGesture="Ctrl + M" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Command="{Binding TakeScreenshot}"
|
||||
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"
|
||||
Header="{ext:Locale MenuBar_Actions_TakeScreenshotButton}"
|
||||
Icon="{ext:Icon fa-solid fa-camera}"
|
||||
InputGesture="{Binding ScreenshotKey}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
Command="{Binding HideUi}"
|
||||
Header="{ext:Locale MenuBarFileToolsHideUi}"
|
||||
Header="{ext:Locale MenuBar_Actions_HideUiButton}"
|
||||
Icon="{ext:Icon fa-solid fa-eye-slash}"
|
||||
InputGesture="{Binding ShowUiKey}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<Separator IsVisible="{Binding RenderDocIsAvailable}" />
|
||||
<MenuItem
|
||||
Name="CheatManagerMenuItem"
|
||||
Header="{ext:Locale GameListContextMenuManageCheat}"
|
||||
Icon="{ext:Icon fa-solid fa-code}"
|
||||
IsVisible="{Binding ShowStartCaptureButton}"
|
||||
Command="{Binding StartRenderDocCapture}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale MenuBar_Actions_StartRenderDocCaptureButton}"
|
||||
Icon="{ext:Icon fa-solid fa-video}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding ShowEndCaptureButton}"
|
||||
Command="{Binding EndRenderDocCapture}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale MenuBar_Actions_EndRenderDocCaptureButton}"
|
||||
Icon="{ext:Icon fa-solid fa-video-slash}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding ShowEndCaptureButton}"
|
||||
Command="{Binding DiscardRenderDocCapture}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale MenuBar_Actions_DiscardRenderDocCaptureButton}"
|
||||
ToolTip.Tip="{ext:Locale MenuBar_Actions_DiscardRenderDocCaptureToolTip}"
|
||||
Icon="{ext:Icon fa-solid fa-video-slash}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarActions}" IsVisible="{Binding EnableNonGameRunningControls}">
|
||||
<MenuItem Header="{ext:Locale MenuBarActionsInstallKeys}" Icon="{ext:Icon fa-solid fa-key}">
|
||||
<MenuItem Command="{Binding InstallKeysFromFile}" Header="{ext:Locale MenuBarFileActionsInstallKeysFromFile}" Icon="{ext:Icon fa-solid fa-file-code}" />
|
||||
<MenuItem Command="{Binding InstallKeysFromFolder}" Header="{ext:Locale MenuBarFileActionsInstallKeysFromFolder}" Icon="{ext:Icon fa-solid fa-folder-closed}" />
|
||||
<MenuItem Header="{ext:Locale MenuBar_Actions_InstallKeysLabel}" Icon="{ext:Icon fa-solid fa-key}">
|
||||
<MenuItem Command="{Binding InstallKeysFromFile}" Header="{ext:Locale MenuBar_Actions_InstallKeysFromFileButton}" Icon="{ext:Icon fa-solid fa-file-code}" />
|
||||
<MenuItem Command="{Binding InstallKeysFromFolder}" Header="{ext:Locale MenuBar_Actions_InstallKeysFromFolderButton}" Icon="{ext:Icon fa-solid fa-folder-closed}" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="{ext:Locale MenuBarActionsInstallFirmware}" Icon="{ext:Icon fa-solid fa-floppy-disk}">
|
||||
<MenuItem Command="{Binding InstallFirmwareFromFile}" Header="{ext:Locale MenuBarActionsInstallFirmwareFromFile}" Icon="{ext:Icon fa-solid fa-file-code}" />
|
||||
<MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarActionsInstallFirmwareFromDirectory}" Icon="{ext:Icon fa-solid fa-folder-closed}" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="{ext:Locale MenuBarActionsManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}" Icon="{ext:Icon fa-solid fa-clipboard}">
|
||||
<MenuItem Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsInstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" Icon="{ext:Icon fa-solid fa-square-plus}" />
|
||||
<MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsUninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" Icon="{ext:Icon fa-solid fa-square-minus}" />
|
||||
<MenuItem Header="{ext:Locale MenuBar_Actions_InstallFirmwareLabel}" Icon="{ext:Icon fa-solid fa-floppy-disk}">
|
||||
<MenuItem Command="{Binding InstallFirmwareFromFile}" Header="{ext:Locale MenuBar_Actions_InstallFirmwareFromFileButton}" Icon="{ext:Icon fa-solid fa-file-code}" />
|
||||
<MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBar_Actions_InstallFirmwareFromFolderButton}" Icon="{ext:Icon fa-solid fa-folder-closed}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="{ext:Locale MenuBarActionsTools}" Icon="{ext:Icon fa-solid fa-toolbox}">
|
||||
<MenuItem Header="{ext:Locale MenuBar_Actions_ToolsLabel}" Icon="{ext:Icon fa-solid fa-toolbox}">
|
||||
<MenuItem
|
||||
Name="MiiAppletMenuItem" Header="{ext:Locale MenuBarActionsOpenMiiEditor}" Icon="{ext:Icon fa-solid fa-face-grin-wide}" ToolTip.Tip="{ext:Locale MenuBarActionsOpenMiiEditorToolTip}" />
|
||||
<MenuItem Name="XciTrimmerMenuItem" Header="{ext:Locale MenuBarActionsXCITrimmer}" Icon="{ext:Icon fa-solid fa-scissors}" />
|
||||
Name="MiiAppletMenuItem" Header="{ext:Locale MenuBar_Actions_MiiEditorButton}" Icon="{ext:Icon fa-solid fa-face-grin-wide}" />
|
||||
<MenuItem Name="XciTrimmerMenuItem" Header="{ext:Locale MenuBar_Actions_XCITrimmerButton}" Icon="{ext:Icon fa-solid fa-scissors}" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarView}">
|
||||
|
||||
@@ -43,7 +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());
|
||||
CheatManagerMenuItem.Command = Commands.CreateSilentFail(OpenCheatManagerForCurrentApp);
|
||||
RestartEmulationMenuItem.Command = Commands.Create(() => ViewModel.RestartEmulation());
|
||||
InstallFileTypesMenuItem.Command = Commands.Create(InstallFileTypes);
|
||||
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
|
||||
XciTrimmerMenuItem.Command = Commands.Create(XciTrimmerView.Show);
|
||||
@@ -101,7 +101,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
Padding = new Thickness(15, 0, 0, 0),
|
||||
Margin = new Thickness(3, 0, 3, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
Header = language == currentLanguageCode ? $"{languageName} ✔" : languageName,
|
||||
Header = code == currentLanguageCode ? $"{languageName} ✔" : languageName,
|
||||
Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(code))
|
||||
};
|
||||
|
||||
@@ -163,24 +163,6 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData);
|
||||
}
|
||||
|
||||
public async Task OpenCheatManagerForCurrentApp()
|
||||
{
|
||||
if (!ViewModel.IsGameRunning)
|
||||
return;
|
||||
|
||||
string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString();
|
||||
|
||||
await StyleableAppWindow.ShowAsync(
|
||||
new CheatWindow(
|
||||
Window.VirtualFileSystem,
|
||||
ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText,
|
||||
name,
|
||||
ViewModel.SelectedApplication.Path)
|
||||
);
|
||||
|
||||
ViewModel.AppHost.Device.EnableCheats();
|
||||
}
|
||||
|
||||
private void ScanAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem)
|
||||
@@ -193,6 +175,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();
|
||||
|
||||
@@ -329,7 +329,7 @@
|
||||
Margin="5, 0, 0, 0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Text="{ext:Locale StatusBarSystemVersion}" />
|
||||
Text="{ext:Locale StatusBar_FirmwareVersion}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
<ComboBoxItem
|
||||
IsEnabled="{Binding IsSDL3Enabled}"
|
||||
Content="{ext:Locale SettingsTabSystemAudioBackendSDL3}" />
|
||||
<ComboBoxItem
|
||||
IsEnabled="{Binding IsAudioToolboxEnabled}"
|
||||
Content="{ext:Locale SettingsTabSystemAudioBackendAudioToolbox}" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
Click="SaveButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfilesSetProfileImage}" />
|
||||
<TextBlock Text="{ext:Locale UserProfilesSave}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -78,22 +78,16 @@
|
||||
Spacing="10"
|
||||
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
|
||||
Content="{ext:Locale AvatarChoose}"
|
||||
Height="35"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user