Compare commits

..

125 Commits

Author SHA1 Message Date
Neo
ecd503cf20 Merge branch ryujinx:master into ui-userprofiles-and-misc 2026-02-10 02:13:32 -06:00
shinyoyo
1260f93aaf Updated ‌Simplified Chinese‌ translation. (ryubing/ryujinx!260)
See merge request ryubing/ryujinx!260
2026-02-09 01:07:22 -06:00
Neo
36d9466427 Merge branch ryujinx:master into ui-userprofiles-and-misc 2026-02-01 03:06:19 -06:00
LotP
1b3bf1473d Fix Dual Joy-Con driver and InputView (ryubing/ryujinx!259)
See merge request ryubing/ryujinx!259
2026-01-31 23:12:29 -06:00
LotP
081cdcab0c remap joy-cons (ryubing/ryujinx!258)
See merge request ryubing/ryujinx!258
2026-01-31 17:58:31 -06:00
Coxxs
922775664c audio: Fix crash due to invalid Splitter size (ryubing/ryujinx!257)
See merge request ryubing/ryujinx!257
2026-01-31 11:22:14 -06:00
Neo
6282db2cb0 Merge branch ryujinx:master into ui-userprofiles-and-misc 2026-01-31 05:10:59 -06:00
sh0inx
478b66fd49 HLE: Stubbed IUserLocalCommuniationService SetProtocol (106) (ryubing/ryujinx!253)
See merge request ryubing/ryujinx!253
2026-01-30 20:48:41 -06:00
Neo
6ea4ce3404 Merge branch ryujinx:master into ui-userprofiles-and-misc 2026-01-30 06:18:03 -06:00
Coxxs
a16a072155 HLE: Implement 10106 and 10107 in IPrepoService (ryubing/ryujinx!254)
See merge request ryubing/ryujinx!254
2026-01-29 13:45:35 -06:00
Neo
72c88b9b50 Merge branch ryujinx:master into ui-userprofiles-and-misc 2026-01-29 09:01:47 -06:00
Babib3l
a4a0fcd4da General translations updates + fixes (ryubing/ryujinx!248)
See merge request ryubing/ryujinx!248
2026-01-28 07:01:39 -06:00
GreemDev
cc5b60bbca fix AppleHardwareDeviceDriver.IsSupported (no fancy check is needed; it's on any macOS version 10.5 (Leopard) and above) 2026-01-28 00:05:02 -06:00
GreemDev
5ed94c365b add a stack trace for the catch branch of AppleHardwareDeviceDriver.IsSupported 2026-01-27 17:52:45 -06:00
GreemDev
fef93a453a [ci skip] replace all usages of IntPtr with nint 2026-01-27 17:41:46 -06:00
GreemDev
82074eb191 audio backend projects code cleanup 2026-01-27 17:34:51 -06:00
GreemDev
bd388cf4f9 Expose AudioToolkit in UI 2026-01-27 17:28:59 -06:00
Stossy11
d271abe19a [ci skip] Add macOS native Audio Backend (ryubing/ryujinx!252)
See merge request ryubing/ryujinx!252

THIS IS CURRENTLY NOT EXPOSED BY THE UI OR HANDLED BY THE EMULATOR. Expect a commit later to add it to configs, UI, etc.
2026-01-27 17:03:59 -06:00
Neo
0baf3b84ed Merge branch ryujinx:master into ui-userprofiles-and-misc 2026-01-22 05:19:01 -06:00
Hack茶ん
c154f66f26 Update Korean translation (ryubing/ryujinx!251)
See merge request ryubing/ryujinx!251
2026-01-21 18:23:34 -06:00
Neo
1552d31e01 Merge branch ryujinx:master into ui-userprofiles-and-misc 2026-01-21 04:07:47 -06:00
GreemDev
f556e8b8fb add offline update server catch branch 2026-01-20 13:19:44 -06:00
_Neo_
5d0fc6d456 Minor cleanup
Going over changed files and adjusting structure and looks
2026-01-17 17:21:37 +02:00
_Neo_
5993c78344 Another entry to UserProfiles.json 2026-01-16 19:45:51 +02:00
_Neo_
e1b01f2e70 Move another entry to UserProfiles.json 2026-01-16 19:29:31 +02:00
_Neo_
e3bdda7afa Remove duplicate locale of "Choose Avatar" 2026-01-16 19:27:23 +02:00
_Neo_
0d5503c014 Add back the file picker options 2026-01-16 19:00:21 +02:00
_Neo_
b26375cc4b Some more adjustments 2026-01-06 13:08:48 +02:00
_Neo_
85dad79581 Reverts & Some Fixes 2026-01-06 13:01:29 +02:00
_Neo_
2941951f4d Fractured locales minor cleanup 2026-01-06 11:47:32 +02:00
_Neo_
925ae1652b Some fixes
Alignment, sizing, and selection indicator
2026-01-05 17:30:16 +02:00
_Neo_
af59454e19 Implement Fractured Locales for User Profiles
Initial implementation before cleanup
2026-01-04 17:40:09 +02:00
Neo
beb7dfe7a6 Merge branch ryujinx:master into ui-userprofiles-and-misc 2026-01-03 12:42:07 -06:00
Babib3l
99feaafbe6 French and Spanish Translations updates on RenderDoc (ryubing/ryujinx!246)
See merge request ryubing/ryujinx!246
2026-01-03 07:23:11 -06:00
Neo
85e62c3ad2 Merge branch ryujinx:master into ui-userprofiles-and-misc 2026-01-01 04:59:59 -06:00
_Neo_
082a2a2051 Quick fix 2025-12-29 21:05:13 +02:00
_Neo_
2b2159f330 Proper UserSelector & ProfileSelector Alignment 2025-12-29 21:02:44 +02:00
_Neo_
3620c76cc1 Initial cleanup 2025-12-29 12:36:37 +02:00
Neo
5a8f6fa46d Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-28 07:05:46 -06:00
_Neo_
813057acbf Further adjustments to FirmwareAvatarSelector 2025-12-26 17:54:01 +02:00
_Neo_
620eba5fcb Removals and cleanup 2025-12-26 17:23:23 +02:00
Neo
829a5561ab Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-26 02:54:13 -06:00
_Neo_
0aec32f437 Adding an observable property 2025-12-25 19:39:01 +02:00
_Neo_
6619de59ab Testing new stuff 2025-12-25 19:37:29 +02:00
_Neo_
746dc2cd22 Margins + Locale adjustments Pt.2 2025-12-24 21:16:13 +02:00
_Neo_
17ab09119e Margins + Locale adjustments 2025-12-24 18:55:08 +02:00
Neo
46bae1c40a Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-23 02:27:12 -06:00
Neo
5536bba1fa Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-22 02:44:23 -06:00
_Neo_
8c6b642129 Center listbox elements in ProfileSelectorDialog 2025-12-20 16:36:51 +02:00
Neo
76dd747811 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-19 04:58:26 -06:00
Neo
1c073ebc63 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-18 03:52:22 -06:00
Neo
cebe423df2 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-13 05:08:36 -06:00
Neo
93f53b24b8 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-07 03:33:08 -06:00
Neo
cb5c9ce585 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-05 08:06:27 -06:00
Neo
167b41354b Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-05 04:07:13 -06:00
Neo
e52fa0b9d1 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-12-03 07:17:01 -06:00
Neo
6ed92dd9b7 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-20 02:51:42 -06:00
Neo
d9846faa5f Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-18 02:21:28 -06:00
_Neo_
928a189d99 Fix "No Profiles To Recover" not updating in certain instances
"No Profiles To Recover" text did not show up after saving the recovered profile, as the user is still on the "Profile Recovery" window. This now updates this.
2025-11-17 13:43:23 +02:00
_Neo_
de11115971 Remove "Profile" from "Set Profile Image" + Others 2025-11-17 13:32:57 +02:00
Neo
c093b34767 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-17 02:42:27 -06:00
Neo
13036dcd5b Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-16 02:22:28 -06:00
Neo
51bec1f4a2 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-15 03:23:23 -06:00
Neo
00cb9e42f8 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-14 05:54:46 -06:00
Neo
5be56d0ccf Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-12 08:57:43 -06:00
Neo
a9bb932491 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-11 13:26:11 -06:00
Neo
e1f215de46 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-11 13:03:34 -06:00
Neo
b6eb78598c Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-11 02:37:12 -06:00
Neo
1392fcfbc5 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-10 03:14:11 -06:00
Neo
9b82e8452f Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-08 10:01:40 -06:00
Neo
379ce9e7aa Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-08 03:13:13 -06:00
Neo
fc89c17037 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-07 03:17:31 -06:00
Neo
222db1a736 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-05 03:47:22 -06:00
_Neo_
89c6c490a3 Merge branch 'ui-userprofiles-and-misc' of https://git.ryujinx.app/neo/ryujinx into ui-userprofiles-and-misc 2025-11-01 13:33:54 +02:00
_Neo_
4d3a98e71d Fixing ProfileSelectorDialog and removing unnecessary style which literally does nothing visually 2025-11-01 13:33:33 +02:00
Neo
18233cf7e6 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-11-01 05:21:36 -05:00
_Neo_
c0cc54cc56 Reverting "I want to reset my settings" and fixing merge conflicts 2025-11-01 12:21:25 +02:00
Neo
d9ab68b1e9 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-30 03:28:45 -05:00
Neo
e7e0d4d877 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-29 02:32:24 -05:00
_Neo_
a2fa346cfd Remove unnecessary Close in User Profiles 2025-10-28 15:49:11 +02:00
_Neo_
6ae279300c Remove unused line 2025-10-28 12:27:16 +02:00
Neo
82e392604d Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-28 04:45:57 -05:00
Neo
860112c910 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-27 05:46:55 -05:00
Neo
0ecef83316 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-26 14:31:54 -05:00
_Neo_
93256afd24 Dynamic Window Titles + Clean Up Locales 2025-10-26 17:48:55 +02:00
Neo
5d3f22ac57 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-26 04:46:37 -05:00
_Neo_
5b63aabe8b Update "Lost Accounts" window title with new message 2025-10-25 19:26:54 +03:00
_Neo_
9b6dfab66e Quick fix for French and Spanish "Lost Accounts" 2025-10-25 17:22:18 +03:00
_Neo_
9e1ee169d9 Image Selector Updates 2025-10-25 17:12:45 +03:00
_Neo_
0958796a29 New Edits + Potential Fixes 2025-10-25 16:50:30 +03:00
_Neo_
f257481cdb Reverting more new stuff... 2025-10-25 15:37:22 +03:00
_Neo_
6533270499 Another revert. 2025-10-25 15:13:05 +03:00
_Neo_
c3c6f36fea Merge branch 'ui-userprofiles-and-misc' of https://git.ryujinx.app/neo/ryujinx into ui-userprofiles-and-misc 2025-10-25 15:11:33 +03:00
_Neo_
df153efadf Reverting... 2025-10-25 15:11:12 +03:00
Neo
4b2362f18b Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-25 07:06:47 -05:00
_Neo_
c2fc1a8582 Fixing... 2025-10-25 15:06:22 +03:00
_Neo_
598d6076ee Fixing Pt.6 2025-10-25 15:05:27 +03:00
_Neo_
69e2ea2894 Fixing Pt.5 2025-10-25 15:02:12 +03:00
_Neo_
93fe2d36aa Fixing... pt.4 2025-10-25 15:01:25 +03:00
_Neo_
0d2f280303 Fixing pt.3 2025-10-25 14:57:35 +03:00
_Neo_
24ac55f4d6 Fixing pt.2 2025-10-25 14:55:59 +03:00
_Neo_
af5d9a90b7 Fixing the MR due to errors in merge 2025-10-25 14:52:58 +03:00
_Neo_
d52415b535 Fix Recover Account window title behaviour 2025-10-23 21:57:39 +03:00
Neo
4d56f4dcd3 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-23 10:11:31 -05:00
Neo
57c91089f7 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-22 02:54:18 -05:00
_Neo_
15c9d50815 Recover Button Fix 2025-10-21 22:31:21 +03:00
Neo
4012fecc25 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-21 05:58:41 -05:00
_Neo_
80df6e2336 Remove Name Nesting in User Selector View 2025-10-20 16:28:20 +03:00
_Neo_
9e2837d885 Fixed Styling & (Potential) Fix "No Profiles To Recover" 2025-10-20 15:50:47 +03:00
_Neo_
96028daff1 Merge branch 'ui-userprofiles-and-misc' of https://git.ryujinx.app/neo/ryujinx into ui-userprofiles-and-misc 2025-10-20 14:38:21 +03:00
_Neo_
46fa8c1426 Reverting no periods and other translations 2025-10-20 14:37:53 +03:00
Neo
35aacdb289 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-20 06:36:37 -05:00
_Neo_
7a2802d870 Fixing MR So Nothing Breaks Pt.1 2025-10-20 14:35:58 +03:00
_Neo_
8548d35620 UserSelector Max Width Back at 90 2025-10-18 22:39:35 +03:00
_Neo_
07ef8e9c9a Potential Size Fix. 2025-10-18 21:28:07 +03:00
_Neo_
5963b425d1 One more. 2025-10-18 18:56:53 +03:00
_Neo_
7f5a67434e Additional fix. 2025-10-18 18:47:40 +03:00
Neo
3abfeebd58 Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-18 03:20:24 -05:00
Neo
8b438c69db Merge branch ryujinx:master into ui-userprofiles-and-misc 2025-10-17 04:23:13 -05:00
_Neo_
1d86653c9d Other Misc Updates 2025-10-15 23:08:21 +03:00
_Neo_
79f3ea5cfa Adjusting some minor values 2025-10-15 20:51:43 +03:00
_Neo_
ba656e560b Lowercase "shader" in some lines. 2025-10-15 19:09:43 +03:00
_Neo_
5a6d476490 Other quick adjustments. 2025-10-15 19:07:20 +03:00
_Neo_
16c35344d8 Quick Radius fix. 2025-10-15 19:05:00 +03:00
_Neo_
9f2ab7aa8f Update User Profile Stuff + Misc. 2025-10-15 18:59:13 +03:00
76 changed files with 2777 additions and 2180 deletions

View File

@@ -44,7 +44,7 @@
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" />
<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.4" />
<PackageVersion Include="Gommon" Version="2.8.0.1" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.11.1" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />

View File

@@ -47,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}"
@@ -569,6 +571,8 @@ Global
{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

View File

@@ -7,12 +7,12 @@
"de_DE": "",
"el_GR": "",
"en_US": "Start RenderDoc Frame Capture",
"es_ES": "",
"fr_FR": "",
"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": "",
"ko_KR": "RenderDoc 프레임 캡처 시작",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -21,7 +21,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "启动 RenderDoc 帧捕获",
"zh_TW": ""
}
},
@@ -32,12 +32,12 @@
"de_DE": "",
"el_GR": "",
"en_US": "End RenderDoc Frame Capture",
"es_ES": "",
"fr_FR": "",
"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": "",
"ko_KR": "RenderDoc 프레임 캡처 종료",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -46,7 +46,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "结束 RenderDoc 帧捕获",
"zh_TW": ""
}
},
@@ -57,12 +57,12 @@
"de_DE": "",
"el_GR": "",
"en_US": "Discard RenderDoc Frame Capture",
"es_ES": "",
"fr_FR": "",
"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": "",
"ko_KR": "RenderDoc 프레임 캡처 폐기",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -71,7 +71,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "丢弃 RenderDoc 帧捕获",
"zh_TW": ""
}
},
@@ -82,12 +82,12 @@
"de_DE": "",
"el_GR": "",
"en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.",
"es_ES": "",
"fr_FR": "",
"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": "",
"ko_KR": "현재 활성화된 RenderDoc 프레임 캡처를 종료하고 결과를 즉시 폐기합니다.",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
@@ -96,7 +96,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。",
"zh_TW": ""
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View 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;
}
}
}

View 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;
}
}

View 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);
}
}
}

View 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;
}
}

View File

@@ -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>

View File

@@ -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)
{

View File

@@ -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))
{

View File

@@ -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)
{

View File

@@ -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))
{

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -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))
{

View File

@@ -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

View File

@@ -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;
}

View File

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

View File

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

View File

@@ -30,9 +30,9 @@ namespace ARMeilleure.Common
/// <summary>
/// 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);

View File

@@ -658,7 +658,7 @@ namespace Ryujinx.Graphics.Gpu.Image
bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel;
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))
{

View File

@@ -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);

View File

@@ -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()
{

View File

@@ -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)

View File

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

View File

@@ -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)
{

View File

@@ -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);

View File

@@ -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);
}
}
}
@@ -194,9 +255,21 @@ 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);
}
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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,

View File

@@ -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);

View File

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

View File

@@ -77,12 +77,13 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.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" />

View File

@@ -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;
@@ -949,6 +950,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 +989,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),

View File

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

View File

@@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -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;

View File

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

View File

@@ -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));
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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