mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-06-06 12:29:15 +00:00
Compare commits
124 Commits
Canary-1.3
...
0a59f8d154
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a59f8d154 | ||
|
|
031cd90048 | ||
|
|
8d5adfed14 | ||
|
|
5aab5f205d | ||
|
|
ecd1c1240c | ||
|
|
bfff9ff780 | ||
|
|
3ad4d4a692 | ||
|
|
2a28dbde83 | ||
|
|
70207cd374 | ||
|
|
23b9a47d08 | ||
|
|
fa0696ca27 | ||
|
|
087655972d | ||
|
|
25306c221d | ||
|
|
cd1ce67f89 | ||
|
|
4c38af62ae | ||
|
|
903cb3f22f | ||
|
|
6fe7fb8dcb | ||
|
|
fc357d3ba4 | ||
|
|
d3f6460fdf | ||
|
|
c1845aac4d | ||
|
|
943d69e93d | ||
|
|
ce07a2be68 | ||
|
|
a8c18a9853 | ||
|
|
c8367335d4 | ||
|
|
ca015200f1 | ||
|
|
5809661664 | ||
|
|
9aeb0c8c8c | ||
|
|
d1464eb5f2 | ||
|
|
e4b920002f | ||
|
|
0b02e71a66 | ||
|
|
7becde9d8e | ||
|
|
9a5e4c06af | ||
|
|
cd4aa41a8f | ||
|
|
b3d18f7845 | ||
|
|
3cbe372b18 | ||
|
|
327f90b420 | ||
|
|
818399ecfc | ||
|
|
1e5c4fedbd | ||
|
|
3401c29b81 | ||
|
|
39f58e453b | ||
|
|
84f3ce2ca5 | ||
|
|
2fe5e8c40d | ||
|
|
ebd8cc4f4a | ||
|
|
13c8b57063 | ||
|
|
32f603d7ad | ||
|
|
7df5299d5a | ||
|
|
32ee806070 | ||
|
|
4e81a4c2f4 | ||
|
|
1b7ffbe723 | ||
|
|
9cae62096a | ||
|
|
648b609ebb | ||
|
|
5ae86fc493 | ||
|
|
6f90e47a73 | ||
|
|
ac5f9857e2 | ||
|
|
4b42087bd4 | ||
|
|
d23b2c162b | ||
|
|
80cbf5d1fc | ||
|
|
128e16b9d3 | ||
|
|
cc6d2dc162 | ||
|
|
4ebc318da5 | ||
|
|
00dad0a5e2 | ||
|
|
0fff818fdf | ||
|
|
5eb5eeb285 | ||
|
|
aabbb3c5d2 | ||
|
|
b8d5744fd3 | ||
|
|
5954f8f3b7 | ||
|
|
041c088d61 | ||
|
|
ddd9ba8aba | ||
|
|
f788e1211d | ||
|
|
d34aa0e549 | ||
|
|
fb881986ce | ||
|
|
f5d87f3bb7 | ||
|
|
8ddb0c16c3 | ||
|
|
54f08acf2c | ||
|
|
3c550deeb7 | ||
|
|
f2e2e93ea2 | ||
|
|
3361ad933f | ||
|
|
27c3231433 | ||
|
|
3d25b9940e | ||
|
|
b5f6e68e55 | ||
|
|
69ec2ef1be | ||
|
|
07491eeaf4 | ||
|
|
d9d9c69a15 | ||
|
|
5327853f72 | ||
|
|
1ab78040aa | ||
|
|
726491d0ba | ||
|
|
b1bd469897 | ||
|
|
2c53c5bb06 | ||
|
|
2a74d2284d | ||
|
|
c980dc00aa | ||
|
|
c7c8086f9f | ||
|
|
0c8c1be821 | ||
|
|
1c3ed0d168 | ||
|
|
a605f7fafc | ||
|
|
03ee34e016 | ||
|
|
8dff5a2556 | ||
|
|
75faee906d | ||
|
|
9beb4efb56 | ||
|
|
914d4c8a79 | ||
|
|
2a999912ea | ||
|
|
c02263abd7 | ||
|
|
02c7d0706a | ||
|
|
028425982c | ||
|
|
a2bb436e40 | ||
|
|
9e1f6db406 | ||
|
|
f84ee55307 | ||
|
|
f045f4acd4 | ||
|
|
f389415b0a | ||
|
|
b4bde4ccb8 | ||
|
|
c76eda2c1a | ||
|
|
59eba8f38b | ||
|
|
fc62ae41ae | ||
|
|
127d0c7ac1 | ||
|
|
15b44cfea6 | ||
|
|
07eddefc95 | ||
|
|
e3ea13bc45 | ||
|
|
0684d60c8c | ||
|
|
6bf57c5ffb | ||
|
|
e4a927f7a1 | ||
|
|
e1e4c111d1 | ||
|
|
0b790469a8 | ||
|
|
385e9c869f | ||
|
|
c5528d59a0 | ||
|
|
6f113c4175 |
6
.github/workflows/canary.yml
vendored
6
.github/workflows/canary.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
|||||||
- name: Install gli
|
- name: Install gli
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.31
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
@@ -162,7 +162,7 @@ jobs:
|
|||||||
- name: Install gli
|
- name: Install gli
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.30
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
@@ -215,7 +215,7 @@ jobs:
|
|||||||
- name: Install gli
|
- name: Install gli
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.30
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
- name: Install gli
|
- name: Install gli
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.31
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
@@ -161,7 +161,7 @@ jobs:
|
|||||||
- name: Install gli
|
- name: Install gli
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.30
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
@@ -217,7 +217,7 @@ jobs:
|
|||||||
- name: Install gli
|
- name: Install gli
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.bin
|
mkdir -p $HOME/.bin
|
||||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.30
|
||||||
chmod +x gli
|
chmod +x gli
|
||||||
mv gli $HOME/.bin/
|
mv gli $HOME/.bin/
|
||||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -72,6 +72,9 @@ ipch/
|
|||||||
_ReSharper*/
|
_ReSharper*/
|
||||||
*.[Rr]e[Ss]harper
|
*.[Rr]e[Ss]harper
|
||||||
|
|
||||||
|
#.NET
|
||||||
|
.dotnet-home/
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
# TeamCity is a build add-in
|
||||||
_TeamCity*
|
_TeamCity*
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Avalonia" Version="11.3.6" />
|
<PackageVersion Include="Avalonia" Version="11.3.12" />
|
||||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
|
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
|
||||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.6" />
|
<PackageVersion Include="Avalonia.Desktop" Version="11.3.12" />
|
||||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" />
|
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.12" />
|
||||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.6" />
|
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.12" />
|
||||||
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.6.2" />
|
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.9.4" />
|
||||||
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.6.2" />
|
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.9.4" />
|
||||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
|
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
|
||||||
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
|
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
|
||||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<PackageVersion Include="Concentus" Version="2.2.2" />
|
<PackageVersion Include="Concentus" Version="2.2.2" />
|
||||||
<PackageVersion Include="DiscordRichPresence" Version="1.6.1.70" />
|
<PackageVersion Include="DiscordRichPresence" Version="1.6.1.70" />
|
||||||
<PackageVersion Include="DynamicData" Version="9.4.1" />
|
<PackageVersion Include="DynamicData" Version="9.4.1" />
|
||||||
<PackageVersion Include="FluentAvaloniaUI.NoAnim" Version="2.4.0-build3" />
|
<PackageVersion Include="FluentAvaloniaUI" Version="2.5.0" />
|
||||||
<PackageVersion Include="Humanizer" Version="2.14.1" />
|
<PackageVersion Include="Humanizer" Version="2.14.1" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
|
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
|
||||||
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.2-build3" />
|
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.2-build3" />
|
||||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||||
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" />
|
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.129" />
|
||||||
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
|
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
|
||||||
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
|
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
|
||||||
<PackageVersion Include="Gommon" Version="2.8.0.1" />
|
<PackageVersion Include="Gommon" Version="2.8.0.1" />
|
||||||
@@ -56,7 +56,6 @@
|
|||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||||
<PackageVersion Include="SPB" Version="0.0.4-build32" />
|
<PackageVersion Include="SPB" Version="0.0.4-build32" />
|
||||||
<PackageVersion Include="System.IO.Hashing" Version="9.0.2" />
|
<PackageVersion Include="System.IO.Hashing" Version="9.0.2" />
|
||||||
<PackageVersion Include="System.Management" Version="9.0.2" />
|
|
||||||
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
|
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
1904
assets/Locales/KeyboardLayout.json
Normal file
1904
assets/Locales/KeyboardLayout.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
namespace Ryujinx.Common.Configuration.Hid.Keyboard
|
namespace Ryujinx.Common.Configuration.Hid.Keyboard
|
||||||
{
|
{
|
||||||
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<Key> { }
|
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<PhysicalKey> { }
|
||||||
}
|
}
|
||||||
|
|||||||
142
src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs
Normal file
142
src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Common.Configuration.Hid
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter<PhysicalKey>))]
|
||||||
|
public enum PhysicalKey
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
ShiftLeft,
|
||||||
|
ShiftRight,
|
||||||
|
ControlLeft,
|
||||||
|
ControlRight,
|
||||||
|
AltLeft,
|
||||||
|
AltRight,
|
||||||
|
WinLeft,
|
||||||
|
WinRight,
|
||||||
|
Menu,
|
||||||
|
F1,
|
||||||
|
F2,
|
||||||
|
F3,
|
||||||
|
F4,
|
||||||
|
F5,
|
||||||
|
F6,
|
||||||
|
F7,
|
||||||
|
F8,
|
||||||
|
F9,
|
||||||
|
F10,
|
||||||
|
F11,
|
||||||
|
F12,
|
||||||
|
F13,
|
||||||
|
F14,
|
||||||
|
F15,
|
||||||
|
F16,
|
||||||
|
F17,
|
||||||
|
F18,
|
||||||
|
F19,
|
||||||
|
F20,
|
||||||
|
F21,
|
||||||
|
F22,
|
||||||
|
F23,
|
||||||
|
F24,
|
||||||
|
F25,
|
||||||
|
F26,
|
||||||
|
F27,
|
||||||
|
F28,
|
||||||
|
F29,
|
||||||
|
F30,
|
||||||
|
F31,
|
||||||
|
F32,
|
||||||
|
F33,
|
||||||
|
F34,
|
||||||
|
F35,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Enter,
|
||||||
|
Escape,
|
||||||
|
Space,
|
||||||
|
Tab,
|
||||||
|
BackSpace,
|
||||||
|
Insert,
|
||||||
|
Delete,
|
||||||
|
PageUp,
|
||||||
|
PageDown,
|
||||||
|
Home,
|
||||||
|
End,
|
||||||
|
CapsLock,
|
||||||
|
ScrollLock,
|
||||||
|
PrintScreen,
|
||||||
|
Pause,
|
||||||
|
NumLock,
|
||||||
|
Clear,
|
||||||
|
Keypad0,
|
||||||
|
Keypad1,
|
||||||
|
Keypad2,
|
||||||
|
Keypad3,
|
||||||
|
Keypad4,
|
||||||
|
Keypad5,
|
||||||
|
Keypad6,
|
||||||
|
Keypad7,
|
||||||
|
Keypad8,
|
||||||
|
Keypad9,
|
||||||
|
KeypadDivide,
|
||||||
|
KeypadMultiply,
|
||||||
|
KeypadSubtract,
|
||||||
|
KeypadAdd,
|
||||||
|
KeypadDecimal,
|
||||||
|
KeypadEnter,
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
C,
|
||||||
|
D,
|
||||||
|
E,
|
||||||
|
F,
|
||||||
|
G,
|
||||||
|
H,
|
||||||
|
I,
|
||||||
|
J,
|
||||||
|
K,
|
||||||
|
L,
|
||||||
|
M,
|
||||||
|
N,
|
||||||
|
O,
|
||||||
|
P,
|
||||||
|
Q,
|
||||||
|
R,
|
||||||
|
S,
|
||||||
|
T,
|
||||||
|
U,
|
||||||
|
V,
|
||||||
|
W,
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Z,
|
||||||
|
Number0,
|
||||||
|
Number1,
|
||||||
|
Number2,
|
||||||
|
Number3,
|
||||||
|
Number4,
|
||||||
|
Number5,
|
||||||
|
Number6,
|
||||||
|
Number7,
|
||||||
|
Number8,
|
||||||
|
Number9,
|
||||||
|
Tilde,
|
||||||
|
Grave,
|
||||||
|
Minus,
|
||||||
|
Plus,
|
||||||
|
BracketLeft,
|
||||||
|
BracketRight,
|
||||||
|
Semicolon,
|
||||||
|
Quote,
|
||||||
|
Comma,
|
||||||
|
Period,
|
||||||
|
Slash,
|
||||||
|
BackSlash,
|
||||||
|
Unbound,
|
||||||
|
|
||||||
|
Count,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" />
|
||||||
<PackageReference Include="MsgPack.Cli" />
|
<PackageReference Include="MsgPack.Cli" />
|
||||||
<PackageReference Include="System.Management" />
|
|
||||||
<PackageReference Include="Humanizer" />
|
<PackageReference Include="Humanizer" />
|
||||||
<PackageReference Include="Gommon" />
|
<PackageReference Include="Gommon" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ namespace Ryujinx.Common
|
|||||||
"01001b300b9be000", // Diablo III: Eternal Collection
|
"01001b300b9be000", // Diablo III: Eternal Collection
|
||||||
"010027400cdc6000", // Divinity Original 2 - Definitive Edition
|
"010027400cdc6000", // Divinity Original 2 - Definitive Edition
|
||||||
"01008c8012920000", // Dying Light Platinum Edition
|
"01008c8012920000", // Dying Light Platinum Edition
|
||||||
|
"0100d11013e6a000", // Eschatos
|
||||||
"01001cc01b2d4000", // Goat Simulator 3
|
"01001cc01b2d4000", // Goat Simulator 3
|
||||||
"01003620068ea000", // Hand of Fate 2
|
"01003620068ea000", // Hand of Fate 2
|
||||||
"0100f7e00c70e000", // Hogwarts Legacy
|
"0100f7e00c70e000", // Hogwarts Legacy
|
||||||
@@ -193,9 +194,15 @@ namespace Ryujinx.Common
|
|||||||
"0100d71004694000", // Minecraft
|
"0100d71004694000", // Minecraft
|
||||||
"01007430037f6000", // Monopoly
|
"01007430037f6000", // Monopoly
|
||||||
"0100853015e86000", // No Man's Sky
|
"0100853015e86000", // No Man's Sky
|
||||||
|
"0100f85014ed0000", // No More Heroes
|
||||||
|
"0100463014ed4000", // No More Heroes 2
|
||||||
|
"0100e570094e8000", // Owlboy
|
||||||
"01007bb017812000", // Portal
|
"01007bb017812000", // Portal
|
||||||
"0100abd01785c000", // Portal 2
|
"0100abd01785c000", // Portal 2
|
||||||
|
"01009f100bc52000", // Psikyo Collection 1
|
||||||
|
"01009d400c4a8000", // Psikyo Collection 2
|
||||||
"01008e200c5c2000", // Muse Dash
|
"01008e200c5c2000", // Muse Dash
|
||||||
|
"01005ff002e2a000", // Rayman Legends
|
||||||
"01007820196a6000", // Red Dead Redemption
|
"01007820196a6000", // Red Dead Redemption
|
||||||
"0100e8300a67a000", // Risk
|
"0100e8300a67a000", // Risk
|
||||||
"01002f7013224000", // Rune Factory 5
|
"01002f7013224000", // Rune Factory 5
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ namespace Ryujinx.Common.Utilities
|
|||||||
}
|
}
|
||||||
|
|
||||||
// "dumpable" attribute of the calling process
|
// "dumpable" attribute of the calling process
|
||||||
|
private const int PR_GET_DUMPABLE = 3;
|
||||||
private const int PR_SET_DUMPABLE = 4;
|
private const int PR_SET_DUMPABLE = 4;
|
||||||
|
|
||||||
[DllImport("libc", SetLastError = true)]
|
[LibraryImport("libc", SetLastError = true)]
|
||||||
private static extern int prctl(int option, int arg2);
|
private static partial int prctl(int option, int arg2);
|
||||||
|
|
||||||
public static void SetCoreDumpable(bool dumpable)
|
public static void SetCoreDumpable(bool dumpable)
|
||||||
{
|
{
|
||||||
@@ -36,5 +37,13 @@ namespace Ryujinx.Common.Utilities
|
|||||||
Debug.Assert(result == 0);
|
Debug.Assert(result == 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the below line to display dumpable status in the console:
|
||||||
|
// Console.WriteLine($"{OsUtils.IsCoreDumpable()}");
|
||||||
|
public static bool IsCoreDumpable()
|
||||||
|
{
|
||||||
|
int result = prctl(PR_GET_DUMPABLE, 0);
|
||||||
|
return result == 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,6 +488,8 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
if (keyPaths.Length is 0)
|
if (keyPaths.Length is 0)
|
||||||
throw new FileNotFoundException($"Directory '{keysSource}' contained no '.keys' files.");
|
throw new FileNotFoundException($"Directory '{keysSource}' contained no '.keys' files.");
|
||||||
|
|
||||||
|
List<string> failedFiles = new();
|
||||||
|
|
||||||
foreach (string filePath in keyPaths)
|
foreach (string filePath in keyPaths)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -497,17 +499,20 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Error?.Print(LogClass.Application, e.Message);
|
Logger.Error?.Print(LogClass.Application, e.Message);
|
||||||
|
failedFiles.Add(Path.GetFileName(filePath));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string destPath = Path.Combine(installDirectory, Path.GetFileName(filePath));
|
string destPath = Path.Combine(installDirectory, Path.GetFileName(filePath));
|
||||||
|
|
||||||
if (File.Exists(destPath))
|
|
||||||
File.Delete(destPath);
|
|
||||||
|
|
||||||
File.Copy(filePath, destPath, true);
|
File.Copy(filePath, destPath, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (failedFiles.Count > 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to install the following key files: {string.Join(", ", failedFiles)}");
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,8 +523,6 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
|
|
||||||
FileInfo info = new(keysSource);
|
FileInfo info = new(keysSource);
|
||||||
|
|
||||||
using FileStream file = File.OpenRead(keysSource);
|
|
||||||
|
|
||||||
if (info.Extension is not ".keys")
|
if (info.Extension is not ".keys")
|
||||||
throw new InvalidFirmwarePackageException("Input file extension is not .keys");
|
throw new InvalidFirmwarePackageException("Input file extension is not .keys");
|
||||||
|
|
||||||
@@ -534,10 +537,6 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
|
|
||||||
string dest = Path.Combine(installDirectory, info.Name);
|
string dest = Path.Combine(installDirectory, info.Name);
|
||||||
|
|
||||||
if (File.Exists(dest))
|
|
||||||
File.Delete(dest);
|
|
||||||
|
|
||||||
// overwrite: true seems to not work on its own? https://github.com/Ryubing/Issues/issues/189
|
|
||||||
File.Copy(keysSource, dest, true);
|
File.Copy(keysSource, dest, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1059,7 +1058,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool AreKeysAlredyPresent(string pathToCheck)
|
public static bool AreKeysAlreadyPresent(string pathToCheck)
|
||||||
{
|
{
|
||||||
string[] fileNames = ["prod.keys", "title.keys", "console.keys", "dev.keys"];
|
string[] fileNames = ["prod.keys", "title.keys", "console.keys", "dev.keys"];
|
||||||
foreach (string file in fileNames)
|
foreach (string file in fileNames)
|
||||||
|
|||||||
@@ -246,21 +246,21 @@ namespace Ryujinx.HLE.HOS
|
|||||||
public void InitializeServices()
|
public void InitializeServices()
|
||||||
{
|
{
|
||||||
SmRegistry = new SmRegistry();
|
SmRegistry = new SmRegistry();
|
||||||
SmServer = new ServerBase(KernelContext, "SmServer", () => new IUserInterface(KernelContext, SmRegistry));
|
SmServer = new ServerBase(KernelContext, "Sm", () => new IUserInterface(KernelContext, SmRegistry));
|
||||||
|
|
||||||
// Wait until SM server thread is done with initialization,
|
// Wait until SM server thread is done with initialization,
|
||||||
// only then doing connections to SM is safe.
|
// only then doing connections to SM is safe.
|
||||||
SmServer.InitDone.WaitOne();
|
SmServer.InitDone.WaitOne();
|
||||||
|
|
||||||
BsdServer = new ServerBase(KernelContext, "BsdServer");
|
BsdServer = new ServerBase(KernelContext, "Bsd");
|
||||||
FsServer = new ServerBase(KernelContext, "FsServer");
|
FsServer = new ServerBase(KernelContext, "Fs");
|
||||||
HidServer = new ServerBase(KernelContext, "HidServer");
|
HidServer = new ServerBase(KernelContext, "Hid");
|
||||||
NvDrvServer = new ServerBase(KernelContext, "NvservicesServer");
|
NvDrvServer = new ServerBase(KernelContext, "Nv");
|
||||||
TimeServer = new ServerBase(KernelContext, "TimeServer");
|
TimeServer = new ServerBase(KernelContext, "Time");
|
||||||
ViServer = new ServerBase(KernelContext, "ViServerU");
|
ViServer = new ServerBase(KernelContext, "Vi:u");
|
||||||
ViServerM = new ServerBase(KernelContext, "ViServerM");
|
ViServerM = new ServerBase(KernelContext, "Vi:m");
|
||||||
ViServerS = new ServerBase(KernelContext, "ViServerS");
|
ViServerS = new ServerBase(KernelContext, "Vi:s");
|
||||||
LdnServer = new ServerBase(KernelContext, "LdnServer");
|
LdnServer = new ServerBase(KernelContext, "Ldn");
|
||||||
|
|
||||||
StartNewServices();
|
StartNewServices();
|
||||||
}
|
}
|
||||||
@@ -286,7 +286,7 @@ namespace Ryujinx.HLE.HOS
|
|||||||
ProcessCreationFlags.Is64Bit |
|
ProcessCreationFlags.Is64Bit |
|
||||||
ProcessCreationFlags.PoolPartitionSystem;
|
ProcessCreationFlags.PoolPartitionSystem;
|
||||||
|
|
||||||
ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
|
ProcessCreationInfo creationInfo = new(service.Name, 1, 0, 0x8000000, 1, Flags, 0, 0);
|
||||||
|
|
||||||
uint[] defaultCapabilities =
|
uint[] defaultCapabilities =
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
|||||||
_activeCount = 0;
|
_activeCount = 0;
|
||||||
|
|
||||||
JoyHold = NpadJoyHoldType.Vertical;
|
JoyHold = NpadJoyHoldType.Vertical;
|
||||||
|
SixAxisActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player)
|
internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player)
|
||||||
@@ -581,6 +582,29 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
|||||||
return needUpdateRight;
|
return needUpdateRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool isAtRest(int playerNumber)
|
||||||
|
{
|
||||||
|
ref NpadInternalState currentNpad = ref _device.Hid.SharedMemory.Npads[playerNumber].InternalState;
|
||||||
|
|
||||||
|
if (currentNpad.StyleSet == NpadStyleTag.None)
|
||||||
|
{
|
||||||
|
return true; // it will always be at rest because it cannot move.
|
||||||
|
}
|
||||||
|
|
||||||
|
ref SixAxisSensorState storage = ref GetSixAxisSensorLifo(ref currentNpad, false).GetCurrentEntryRef();
|
||||||
|
|
||||||
|
float acceleration = Math.Abs(storage.Acceleration.X)
|
||||||
|
+ Math.Abs(storage.Acceleration.Y)
|
||||||
|
+ Math.Abs(storage.Acceleration.Z);
|
||||||
|
|
||||||
|
float angularVelocity = Math.Abs(storage.AngularVelocity.X)
|
||||||
|
+ Math.Abs(storage.AngularVelocity.Y)
|
||||||
|
+ Math.Abs(storage.AngularVelocity.Z);
|
||||||
|
|
||||||
|
// TODO: check against config deadzone and add sensitivity setting
|
||||||
|
return ((acceleration <= 1.0F) && (angularVelocity <= 1.0F));
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateDisconnectedInputSixAxis(PlayerIndex index)
|
private void UpdateDisconnectedInputSixAxis(PlayerIndex index)
|
||||||
{
|
{
|
||||||
ref NpadInternalState currentNpad = ref _device.Hid.SharedMemory.Npads[(int)index].InternalState;
|
ref NpadInternalState currentNpad = ref _device.Hid.SharedMemory.Npads[(int)index].InternalState;
|
||||||
|
|||||||
@@ -602,19 +602,33 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
|||||||
}
|
}
|
||||||
|
|
||||||
[CommandCmif(82)]
|
[CommandCmif(82)]
|
||||||
// IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAsRest
|
// IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAtRest
|
||||||
public ResultCode IsSixAxisSensorAtRest(ServiceCtx context)
|
public ResultCode IsSixAxisSensorAtRest(ServiceCtx context)
|
||||||
{
|
{
|
||||||
int sixAxisSensorHandle = context.RequestData.ReadInt32();
|
int sixAxisSensorHandle = context.RequestData.ReadInt32();
|
||||||
|
|
||||||
|
// 4 byte struct w/ 4-byte alignment
|
||||||
|
|
||||||
|
// uint typeValue = (uint) sixAxisSensorHandle; // 0x0 0x4 TypeValue
|
||||||
|
// uint npadStyleIndex = (uint) sixAxisSensorHandle & 0xff; // 0x0 0x1 NpadStyleIndex
|
||||||
|
int playerNumber = (sixAxisSensorHandle << 8) & 0xff; // 0x1 0x1 PlayerNumber
|
||||||
|
// uint deviceIdx= ((uint) sixAxisSensorHandle << 16) & 0xff; // 0x2 0x1 DeviceIdx
|
||||||
|
// uint unknown = ((uint) sixAxisSensorHandle << 24) & 0xff;
|
||||||
|
|
||||||
|
// 32bit sign extension padding -> if = 0, + offset, else - offset
|
||||||
|
|
||||||
|
// npadStyleIndex = ((npadStyleIndex & 0x8000) == 0) ? npadStyleIndex | 0xFFFF0000 : npadStyleIndex & 0xFFFF0000;
|
||||||
|
// playerNumber = ((playerNumber & 0x8000) == 0) ? playerNumber | 0xFFFF0000 : playerNumber & 0xFFFF0000;
|
||||||
|
// deviceIdx = ((deviceIdx & 0x8000) == 0) ? deviceIdx | 0xFFFF0000 : deviceIdx & 0xFFFF0000;
|
||||||
|
// unknown = ((unknown & 0x8000) == 0) ? unknown | 0xFFFF0000 : unknown & 0xFFFF0000;
|
||||||
|
|
||||||
context.RequestData.BaseStream.Position += 4; // Padding
|
context.RequestData.BaseStream.Position += 4; // Padding
|
||||||
long appletResourceUserId = context.RequestData.ReadInt64();
|
long appletResourceUserId = context.RequestData.ReadInt64();
|
||||||
|
|
||||||
bool isAtRest = true;
|
// TODO: link to context.Device.Hid.Npads.SixAxisActive when properly implemented
|
||||||
|
// We currently do not support stopping or starting SixAxisTracking.
|
||||||
context.ResponseData.Write(isAtRest);
|
|
||||||
|
|
||||||
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, isAtRest });
|
|
||||||
|
|
||||||
|
context.ResponseData.Write(context.Device.Hid.Npads.isAtRest(playerNumber));
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,9 +79,15 @@ namespace Ryujinx.HLE.HOS.Services
|
|||||||
ProcessCreationFlags.Is64Bit |
|
ProcessCreationFlags.Is64Bit |
|
||||||
ProcessCreationFlags.PoolPartitionSystem;
|
ProcessCreationFlags.PoolPartitionSystem;
|
||||||
|
|
||||||
ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
|
ProcessCreationInfo creationInfo = new(Name, 1, 0, 0x8000000, 1, Flags, 0, 0);
|
||||||
|
|
||||||
KernelStatic.StartInitialProcess(context, creationInfo, DefaultCapabilities, 44, Main);
|
KernelStatic.StartInitialProcess(context, creationInfo, DefaultCapabilities, 44, () =>
|
||||||
|
{
|
||||||
|
var currentThread = KernelStatic.GetCurrentThread();
|
||||||
|
currentThread.HostThread.Name = $"{{{Name}}}";
|
||||||
|
|
||||||
|
Main();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPort(int serverPortHandle, Func<IpcService> objectFactory)
|
private void AddPort(int serverPortHandle, Func<IpcService> objectFactory)
|
||||||
|
|||||||
@@ -17,13 +17,12 @@ namespace Ryujinx.HLE.HOS.Services.Sm
|
|||||||
private static readonly Dictionary<string, Type> _services;
|
private static readonly Dictionary<string, Type> _services;
|
||||||
|
|
||||||
private readonly SmRegistry _registry;
|
private readonly SmRegistry _registry;
|
||||||
private readonly ServerBase _commonServer;
|
private ServerBase _commonServer;
|
||||||
|
|
||||||
private bool _isInitialized;
|
private bool _isInitialized;
|
||||||
|
|
||||||
public IUserInterface(KernelContext context, SmRegistry registry) : base(registerTipc: true)
|
public IUserInterface(KernelContext context, SmRegistry registry) : base(registerTipc: true)
|
||||||
{
|
{
|
||||||
_commonServer = new ServerBase(context, "CommonServer");
|
|
||||||
_registry = registry;
|
_registry = registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +96,11 @@ namespace Ryujinx.HLE.HOS.Services.Sm
|
|||||||
|
|
||||||
IpcService service = GetServiceInstance(type, context, serviceAttribute.Parameter);
|
IpcService service = GetServiceInstance(type, context, serviceAttribute.Parameter);
|
||||||
|
|
||||||
|
if (_commonServer is null)
|
||||||
|
{
|
||||||
|
_commonServer = new ServerBase(context.Device.System.KernelContext, "Common");
|
||||||
|
}
|
||||||
|
|
||||||
service.TrySetServer(_commonServer);
|
service.TrySetServer(_commonServer);
|
||||||
service.Server.AddSessionObj(session.ServerSession, service);
|
service.Server.AddSessionObj(session.ServerSession, service);
|
||||||
}
|
}
|
||||||
@@ -253,7 +257,7 @@ namespace Ryujinx.HLE.HOS.Services.Sm
|
|||||||
|
|
||||||
public override void DestroyAtExit()
|
public override void DestroyAtExit()
|
||||||
{
|
{
|
||||||
_commonServer.Dispose();
|
_commonServer?.Dispose();
|
||||||
|
|
||||||
base.DestroyAtExit();
|
base.DestroyAtExit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl
|
|||||||
public ISslService(ServiceCtx context) { }
|
public ISslService(ServiceCtx context) { }
|
||||||
|
|
||||||
[CommandCmif(0)]
|
[CommandCmif(0)]
|
||||||
// CreateContext(nn::ssl::sf::SslVersion, u64, pid) -> object<nn::ssl::sf::ISslContext>
|
// CreateContext(nn::ssl::sf::SslVersion, u64 pid_placeholder, pid) -> object<nn::ssl::sf::ISslContext>
|
||||||
public ResultCode CreateContext(ServiceCtx context)
|
public ResultCode CreateContext(ServiceCtx context)
|
||||||
{
|
{
|
||||||
SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32();
|
SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32();
|
||||||
@@ -126,14 +126,18 @@ namespace Ryujinx.HLE.HOS.Services.Ssl
|
|||||||
}
|
}
|
||||||
|
|
||||||
[CommandCmif(100)]
|
[CommandCmif(100)]
|
||||||
// CreateContextForSystem(u64 pid, nn::ssl::sf::SslVersion, u64)
|
// CreateContextForSystem(nn::ssl::sf::SslVersion, u64 pid_placeholder, pid) -> object<nn::ssl::sf::ISslContextForSystem>
|
||||||
public ResultCode CreateContextForSystem(ServiceCtx context)
|
public ResultCode CreateContextForSystem(ServiceCtx context)
|
||||||
{
|
{
|
||||||
ulong pid = context.RequestData.ReadUInt64();
|
|
||||||
SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32();
|
SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32();
|
||||||
|
#pragma warning disable IDE0059 // Remove unnecessary value assignment
|
||||||
ulong pidPlaceholder = context.RequestData.ReadUInt64();
|
ulong pidPlaceholder = context.RequestData.ReadUInt64();
|
||||||
|
#pragma warning restore IDE0059
|
||||||
|
|
||||||
Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { pid, sslVersion, pidPlaceholder });
|
// Note: We use ISslContext here instead of ISslContextForSystem class because Ryujinx implements both in one class.
|
||||||
|
MakeObject(context, new ISslContext(context.Request.HandleDesc.PId, sslVersion));
|
||||||
|
|
||||||
|
Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { sslVersion });
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,5 +20,7 @@ namespace Ryujinx.HLE.HOS.SystemState
|
|||||||
SimplifiedChinese,
|
SimplifiedChinese,
|
||||||
TraditionalChinese,
|
TraditionalChinese,
|
||||||
BrazilianPortuguese,
|
BrazilianPortuguese,
|
||||||
|
Polish,
|
||||||
|
Thai,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ namespace Ryujinx.HLE.HOS.SystemState
|
|||||||
"es-419",
|
"es-419",
|
||||||
"zh-Hans",
|
"zh-Hans",
|
||||||
"zh-Hant",
|
"zh-Hant",
|
||||||
"pt-BR"
|
"pt-BR",
|
||||||
|
"pl",
|
||||||
|
"th"
|
||||||
];
|
];
|
||||||
|
|
||||||
internal long DesiredKeyboardLayout { get; private set; }
|
internal long DesiredKeyboardLayout { get; private set; }
|
||||||
|
|||||||
@@ -18,5 +18,7 @@ namespace Ryujinx.HLE.HOS.SystemState
|
|||||||
TraditionalChinese,
|
TraditionalChinese,
|
||||||
SimplifiedChinese,
|
SimplifiedChinese,
|
||||||
BrazilianPortuguese,
|
BrazilianPortuguese,
|
||||||
|
Polish,
|
||||||
|
Thai,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
using Ryujinx.Common.Memory;
|
using Ryujinx.Common.Memory;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Ryujinx.Horizon.Sdk.Ns
|
namespace Ryujinx.Horizon.Sdk.Ns
|
||||||
{
|
{
|
||||||
public struct ApplicationControlProperty
|
public struct ApplicationControlProperty
|
||||||
{
|
{
|
||||||
public Array16<ApplicationTitle> Title;
|
/// <summary>
|
||||||
|
/// Use <see cref="Title"/> to access titles instead of accessing them directly.
|
||||||
|
/// </summary>
|
||||||
|
public Array16<ApplicationTitle> TitleBlock;
|
||||||
public Array37<byte> Isbn;
|
public Array37<byte> Isbn;
|
||||||
public StartupUserAccountValue StartupUserAccount;
|
public StartupUserAccountValue StartupUserAccount;
|
||||||
public UserAccountSwitchLockValue UserAccountSwitchLock;
|
public UserAccountSwitchLockValue UserAccountSwitchLock;
|
||||||
@@ -58,7 +65,10 @@ namespace Ryujinx.Horizon.Sdk.Ns
|
|||||||
public RepairFlagValue RepairFlag;
|
public RepairFlagValue RepairFlag;
|
||||||
public byte ProgramIndex;
|
public byte ProgramIndex;
|
||||||
public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag;
|
public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag;
|
||||||
public Array4<byte> Reserved3214;
|
public byte ApplicationErrorCodePrefix;
|
||||||
|
public TitleCompressionValue TitleCompression;
|
||||||
|
public byte AcdIndex;
|
||||||
|
public byte ApparentPlatform;
|
||||||
public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration;
|
public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration;
|
||||||
public ApplicationJitConfiguration JitConfiguration;
|
public ApplicationJitConfiguration JitConfiguration;
|
||||||
public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors;
|
public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors;
|
||||||
@@ -75,6 +85,47 @@ namespace Ryujinx.Horizon.Sdk.Ns
|
|||||||
public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0');
|
public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0');
|
||||||
public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0');
|
public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0');
|
||||||
|
|
||||||
|
private const int TitleCount = 32;
|
||||||
|
private const int TitleEntrySize = 0x300;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the resolved title entries. When <see cref="TitleCompression"/> is
|
||||||
|
/// <see cref="TitleCompressionValue.Enable"/>, the raw <see cref="TitleBlock"/> bytes are
|
||||||
|
/// decompressed (raw deflate) from 0x3000 into 0x6000 bytes yielding up to 32 entries.
|
||||||
|
/// Otherwise the 16 uncompressed entries from <see cref="TitleBlock"/> are returned directly.
|
||||||
|
/// </summary>
|
||||||
|
public readonly ApplicationTitle[] Title
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var titles = new ApplicationTitle[TitleCount];
|
||||||
|
|
||||||
|
if (TitleCompression != TitleCompressionValue.Enable)
|
||||||
|
{
|
||||||
|
TitleBlock.AsSpan().CopyTo(titles);
|
||||||
|
|
||||||
|
return titles;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<byte> titleBytes = MemoryMarshal.AsBytes(TitleBlock.AsSpan());
|
||||||
|
ushort compressedBlobSize = BinaryPrimitives.ReadUInt16LittleEndian(titleBytes);
|
||||||
|
ReadOnlySpan<byte> compressedBlob = titleBytes.Slice(2, compressedBlobSize);
|
||||||
|
|
||||||
|
byte[] decompressed = new byte[TitleCount * TitleEntrySize];
|
||||||
|
|
||||||
|
using (var compressedStream = new MemoryStream(compressedBlob.ToArray()))
|
||||||
|
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
|
||||||
|
{
|
||||||
|
deflateStream.ReadExactly(decompressed, 0, decompressed.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryMarshal.Cast<byte, ApplicationTitle>(decompressed).CopyTo(titles);
|
||||||
|
|
||||||
|
return titles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct ApplicationTitle
|
public struct ApplicationTitle
|
||||||
{
|
{
|
||||||
public ByteArray512 Name;
|
public ByteArray512 Name;
|
||||||
@@ -130,6 +181,8 @@ namespace Ryujinx.Horizon.Sdk.Ns
|
|||||||
TraditionalChinese = 13,
|
TraditionalChinese = 13,
|
||||||
SimplifiedChinese = 14,
|
SimplifiedChinese = 14,
|
||||||
BrazilianPortuguese = 15,
|
BrazilianPortuguese = 15,
|
||||||
|
Polish = 16,
|
||||||
|
Thai = 17,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Organization
|
public enum Organization
|
||||||
@@ -302,5 +355,11 @@ namespace Ryujinx.Horizon.Sdk.Ns
|
|||||||
Deny = 0,
|
Deny = 0,
|
||||||
Allow = 1,
|
Allow = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum TitleCompressionValue : byte
|
||||||
|
{
|
||||||
|
Disable = 0,
|
||||||
|
Enable = 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ namespace Ryujinx.Horizon
|
|||||||
private readonly Action<ServiceTable> _entrypoint;
|
private readonly Action<ServiceTable> _entrypoint;
|
||||||
private readonly ServiceTable _serviceTable;
|
private readonly ServiceTable _serviceTable;
|
||||||
private readonly HorizonOptions _options;
|
private readonly HorizonOptions _options;
|
||||||
|
public readonly string Name;
|
||||||
|
|
||||||
internal ServiceEntry(Action<ServiceTable> entrypoint, ServiceTable serviceTable, HorizonOptions options)
|
internal ServiceEntry(Action<ServiceTable> entrypoint, ServiceTable serviceTable, HorizonOptions options, string name)
|
||||||
{
|
{
|
||||||
_entrypoint = entrypoint;
|
_entrypoint = entrypoint;
|
||||||
_serviceTable = serviceTable;
|
_serviceTable = serviceTable;
|
||||||
_options = options;
|
_options = options;
|
||||||
|
Name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start(ISyscallApi syscallApi, IVirtualMemoryManager addressSpace, IThreadContext threadContext)
|
public void Start(ISyscallApi syscallApi, IVirtualMemoryManager addressSpace, IThreadContext threadContext)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ namespace Ryujinx.Horizon
|
|||||||
|
|
||||||
void RegisterService<T>() where T : IService
|
void RegisterService<T>() where T : IService
|
||||||
{
|
{
|
||||||
entries.Add(new ServiceEntry(T.Main, this, options));
|
entries.Add(new ServiceEntry(T.Main, this, options, typeof(T).Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterService<ArpMain>();
|
RegisterService<ArpMain>();
|
||||||
|
|||||||
@@ -8,25 +8,15 @@ using System.Runtime.CompilerServices;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SDL;
|
using SDL;
|
||||||
using static SDL.SDL3;
|
using static SDL.SDL3;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
|
|
||||||
|
|
||||||
namespace Ryujinx.Input.SDL3
|
namespace Ryujinx.Input.SDL3
|
||||||
{
|
{
|
||||||
class SDL3Keyboard : IKeyboard
|
class SDL3Keyboard : IKeyboard
|
||||||
{
|
{
|
||||||
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, Key From)
|
|
||||||
{
|
|
||||||
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not Key.Unbound;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Lock _userMappingLock = new();
|
private readonly Lock _userMappingLock = new();
|
||||||
|
|
||||||
#pragma warning disable IDE0052 // Remove unread private member
|
|
||||||
private readonly SDL3KeyboardDriver _driver;
|
|
||||||
#pragma warning restore IDE0052
|
|
||||||
private StandardKeyboardInputConfig _configuration;
|
private StandardKeyboardInputConfig _configuration;
|
||||||
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
|
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
|
||||||
|
|
||||||
|
|
||||||
private static readonly SDL_Keycode[] _keysDriverMapping =
|
private static readonly SDL_Keycode[] _keysDriverMapping =
|
||||||
@@ -171,9 +161,8 @@ namespace Ryujinx.Input.SDL3
|
|||||||
SDL_Keycode.SDLK_0
|
SDL_Keycode.SDLK_0
|
||||||
];
|
];
|
||||||
|
|
||||||
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
|
public SDL3Keyboard(string id, string name)
|
||||||
{
|
{
|
||||||
_driver = driver;
|
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
_buttonsUserMapping = [];
|
_buttonsUserMapping = [];
|
||||||
@@ -195,9 +184,9 @@ namespace Ryujinx.Input.SDL3
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private unsafe static int ToSDL3Scancode(Key key)
|
private unsafe static int ToSDL3Scancode(ConfigPhysicalKey key)
|
||||||
{
|
{
|
||||||
if (key is >= Key.Unknown and <= Key.Menu)
|
if (key is >= ConfigPhysicalKey.Unknown and <= ConfigPhysicalKey.Menu)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -205,18 +194,18 @@ namespace Ryujinx.Input.SDL3
|
|||||||
return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null);
|
return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SDL_Keymod GetKeyboardModifierMask(Key key)
|
private static SDL_Keymod GetKeyboardModifierMask(ConfigPhysicalKey key)
|
||||||
{
|
{
|
||||||
return key switch
|
return key switch
|
||||||
{
|
{
|
||||||
Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
|
ConfigPhysicalKey.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
|
||||||
Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
|
ConfigPhysicalKey.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
|
||||||
Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
|
ConfigPhysicalKey.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
|
||||||
Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
|
ConfigPhysicalKey.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
|
||||||
Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
|
ConfigPhysicalKey.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
|
||||||
Key.AltRight => SDL_Keymod.SDL_KMOD_RALT,
|
ConfigPhysicalKey.AltRight => SDL_Keymod.SDL_KMOD_RALT,
|
||||||
Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
|
ConfigPhysicalKey.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
|
||||||
Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
|
ConfigPhysicalKey.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
|
||||||
// NOTE: Menu key isn't supported by SDL3.
|
// NOTE: Menu key isn't supported by SDL3.
|
||||||
_ => SDL_Keymod.SDL_KMOD_NONE
|
_ => SDL_Keymod.SDL_KMOD_NONE
|
||||||
};
|
};
|
||||||
@@ -232,9 +221,9 @@ namespace Ryujinx.Input.SDL3
|
|||||||
rawKeyboardState = SDL_GetKeyboardState(null);
|
rawKeyboardState = SDL_GetKeyboardState(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool[] keysState = new bool[(int)Key.Count];
|
bool[] keysState = new bool[(int)ConfigPhysicalKey.Count];
|
||||||
|
|
||||||
for (Key key = 0; key < Key.Count; key++)
|
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
|
||||||
{
|
{
|
||||||
int index = ToSDL3Scancode(key);
|
int index = ToSDL3Scancode(key);
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
@@ -264,36 +253,6 @@ namespace Ryujinx.Input.SDL3
|
|||||||
return value * ConvertRate;
|
return value * ConvertRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
|
|
||||||
{
|
|
||||||
short stickX = 0;
|
|
||||||
short stickY = 0;
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickUp))
|
|
||||||
{
|
|
||||||
stickY += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickDown))
|
|
||||||
{
|
|
||||||
stickY -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickRight))
|
|
||||||
{
|
|
||||||
stickX += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
|
|
||||||
{
|
|
||||||
stickX -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
|
|
||||||
|
|
||||||
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
public GamepadStateSnapshot GetMappedStateSnapshot()
|
||||||
{
|
{
|
||||||
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
|
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
|
||||||
@@ -306,9 +265,9 @@ namespace Ryujinx.Input.SDL3
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
|
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
|
||||||
{
|
{
|
||||||
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
|
if (!entry.IsValid)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -320,8 +279,8 @@ namespace Ryujinx.Input.SDL3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
||||||
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
||||||
|
|
||||||
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
||||||
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
||||||
@@ -357,38 +316,15 @@ namespace Ryujinx.Input.SDL3
|
|||||||
{
|
{
|
||||||
_configuration = (StandardKeyboardInputConfig)configuration;
|
_configuration = (StandardKeyboardInputConfig)configuration;
|
||||||
|
|
||||||
// First clear the buttons mapping
|
|
||||||
_buttonsUserMapping.Clear();
|
_buttonsUserMapping.Clear();
|
||||||
|
|
||||||
// Then configure left joycon
|
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
|
|
||||||
|
|
||||||
// Finally configure right joycon
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetLed(uint packedRgb)
|
public void SetLed(uint packedRgb)
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
|
Logger.Debug?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTriggerThreshold(float triggerThreshold)
|
public void SetTriggerThreshold(float triggerThreshold)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace Ryujinx.Input.SDL3
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
|
return new SDL3Keyboard(_keyboardIdentifers[0], "All keyboards");
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IGamepad> GetGamepads()
|
public IEnumerable<IGamepad> GetGamepads()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
|
||||||
namespace Ryujinx.Input.Assigner
|
namespace Ryujinx.Input.Assigner
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -8,22 +10,42 @@ namespace Ryujinx.Input.Assigner
|
|||||||
private readonly IKeyboard _keyboard;
|
private readonly IKeyboard _keyboard;
|
||||||
|
|
||||||
private KeyboardStateSnapshot _keyboardState;
|
private KeyboardStateSnapshot _keyboardState;
|
||||||
|
private Button? _pressedButton;
|
||||||
|
|
||||||
public KeyboardKeyAssigner(IKeyboard keyboard)
|
public KeyboardKeyAssigner(IKeyboard keyboard)
|
||||||
{
|
{
|
||||||
_keyboard = keyboard;
|
_keyboard = keyboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize() { }
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_pressedButton = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void ReadInput()
|
public void ReadInput()
|
||||||
{
|
{
|
||||||
_keyboardState = _keyboard.GetKeyboardStateSnapshot();
|
_keyboardState = _keyboard.GetKeyboardStateSnapshot();
|
||||||
|
|
||||||
|
if (_pressedButton is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button? buttonFromState = GetPressedButtonFromState();
|
||||||
|
Button? buttonFromBufferedPress = buttonFromState is null ? GetPressedButtonFromBufferedPress() : null;
|
||||||
|
|
||||||
|
_pressedButton = buttonFromState ?? buttonFromBufferedPress;
|
||||||
|
|
||||||
|
if (_pressedButton is not null)
|
||||||
|
{
|
||||||
|
string source = buttonFromState is not null ? "state" : "buffered-press";
|
||||||
|
Logger.Debug?.Print(LogClass.UI, $"Keyboard assigner registered key={_pressedButton.Value.AsHidType<Key>()}, source={source}, cancelPressed={ShouldCancel()}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAnyButtonPressed()
|
public bool IsAnyButtonPressed()
|
||||||
{
|
{
|
||||||
return GetPressedButton() is not null;
|
return _pressedButton is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShouldCancel()
|
public bool ShouldCancel()
|
||||||
@@ -33,18 +55,53 @@ namespace Ryujinx.Input.Assigner
|
|||||||
|
|
||||||
public Button? GetPressedButton()
|
public Button? GetPressedButton()
|
||||||
{
|
{
|
||||||
Button? keyPressed = null;
|
return !ShouldCancel() ? _pressedButton : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button? GetPressedButtonFromState()
|
||||||
|
{
|
||||||
|
Key aliasedKey = GetAliasedPressedKey();
|
||||||
|
|
||||||
|
if (aliasedKey != Key.Unknown)
|
||||||
|
{
|
||||||
|
return new Button(aliasedKey);
|
||||||
|
}
|
||||||
|
|
||||||
for (Key key = Key.Unknown; key < Key.Count; key++)
|
for (Key key = Key.Unknown; key < Key.Count; key++)
|
||||||
{
|
{
|
||||||
if (_keyboardState.IsPressed(key))
|
if (_keyboardState.IsPressed(key))
|
||||||
{
|
{
|
||||||
keyPressed = new(key);
|
return new Button(key);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return !ShouldCancel() ? keyPressed : null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button? GetPressedButtonFromBufferedPress()
|
||||||
|
{
|
||||||
|
return _keyboard.TryConsumePressedKey(out Key key) ? new Button(key) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Key GetAliasedPressedKey()
|
||||||
|
{
|
||||||
|
// On some layouts (for example AltGr on Windows), Right Alt is reported as Ctrl+Alt.
|
||||||
|
// Prefer AltRight in that case so the binding reflects the physical key used.
|
||||||
|
if (_keyboardState.IsPressed(Key.ControlLeft) && _keyboardState.IsPressed(Key.AltRight))
|
||||||
|
{
|
||||||
|
return Key.AltRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On some Copilot keyboards, the key in the right-control position is reported as
|
||||||
|
// ShiftLeft+Win+F23. Prefer ControlRight so the binding reflects that physical key.
|
||||||
|
if (_keyboardState.IsPressed(Key.ShiftLeft) &&
|
||||||
|
_keyboardState.IsPressed(Key.F23) &&
|
||||||
|
(_keyboardState.IsPressed(Key.WinLeft) || _keyboardState.IsPressed(Key.WinRight)))
|
||||||
|
{
|
||||||
|
return Key.ControlRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Key.Unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Ryujinx.Common;
|
|||||||
using Ryujinx.Common.Configuration.Hid;
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||||
|
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.HOS.Services.Hid;
|
using Ryujinx.HLE.HOS.Services.Hid;
|
||||||
using System;
|
using System;
|
||||||
@@ -234,7 +235,9 @@ namespace Ryujinx.Input.HLE
|
|||||||
_gamepad?.Dispose();
|
_gamepad?.Dispose();
|
||||||
|
|
||||||
Id = config.Id;
|
Id = config.Id;
|
||||||
_gamepad = GamepadDriver.GetGamepad(Id);
|
_gamepad = config is StandardKeyboardInputConfig && GamepadDriver is IKeyboardModeDriver keyboardModeDriver
|
||||||
|
? keyboardModeDriver.GetKeyboard(Id, KeyboardInputMode.Physical)
|
||||||
|
: GamepadDriver.GetGamepad(Id);
|
||||||
|
|
||||||
UpdateUserConfiguration(config);
|
UpdateUserConfiguration(config);
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
|
|
||||||
private List<InputConfig> _inputConfig;
|
private List<InputConfig> _inputConfig;
|
||||||
|
private List<InputConfig> _requestedInputConfig;
|
||||||
private bool _enableKeyboard;
|
private bool _enableKeyboard;
|
||||||
private bool _enableMouse;
|
private bool _enableMouse;
|
||||||
private Switch _device;
|
private Switch _device;
|
||||||
@@ -52,6 +53,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
_gamepadDriver = gamepadDriver;
|
_gamepadDriver = gamepadDriver;
|
||||||
_mouseDriver = mouseDriver;
|
_mouseDriver = mouseDriver;
|
||||||
_inputConfig = [];
|
_inputConfig = [];
|
||||||
|
_requestedInputConfig = [];
|
||||||
|
|
||||||
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
||||||
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
||||||
@@ -89,14 +91,14 @@ namespace Ryujinx.Input.HLE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
|
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleOnGamepadConnected(string id)
|
private void HandleOnGamepadConnected(string id)
|
||||||
{
|
{
|
||||||
// Force input reload
|
// Force input reload
|
||||||
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
|
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
@@ -127,11 +129,13 @@ namespace Ryujinx.Input.HLE
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
_requestedInputConfig = inputConfig?.ToList() ?? [];
|
||||||
|
|
||||||
NpadController[] oldControllers = _controllers.ToArray();
|
NpadController[] oldControllers = _controllers.ToArray();
|
||||||
|
|
||||||
List<InputConfig> validInputs = [];
|
List<InputConfig> validInputs = [];
|
||||||
|
|
||||||
foreach (InputConfig inputConfigEntry in inputConfig)
|
foreach (InputConfig inputConfigEntry in _requestedInputConfig)
|
||||||
{
|
{
|
||||||
NpadController controller;
|
NpadController controller;
|
||||||
int index = (int)inputConfigEntry.PlayerIndex;
|
int index = (int)inputConfigEntry.PlayerIndex;
|
||||||
@@ -147,7 +151,17 @@ namespace Ryujinx.Input.HLE
|
|||||||
controller = new(_cemuHookClient);
|
controller = new(_cemuHookClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
|
InputConfig activeConfig = inputConfigEntry;
|
||||||
|
bool isValid = DriverConfigurationUpdate(ref controller, activeConfig);
|
||||||
|
|
||||||
|
if (!isValid &&
|
||||||
|
enableKeyboard &&
|
||||||
|
inputConfigEntry is StandardControllerInputConfig &&
|
||||||
|
TryGetKeyboardFallback(inputConfigEntry, out StandardKeyboardInputConfig fallbackConfig))
|
||||||
|
{
|
||||||
|
activeConfig = fallbackConfig;
|
||||||
|
isValid = DriverConfigurationUpdate(ref controller, activeConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isValid)
|
if (!isValid)
|
||||||
{
|
{
|
||||||
@@ -157,7 +171,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_controllers[index] = controller;
|
_controllers[index] = controller;
|
||||||
validInputs.Add(inputConfigEntry);
|
validInputs.Add(activeConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +183,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
oldControllers[i] = null;
|
oldControllers[i] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_inputConfig = inputConfig;
|
_inputConfig = validInputs;
|
||||||
_enableKeyboard = enableKeyboard;
|
_enableKeyboard = enableKeyboard;
|
||||||
_enableMouse = enableMouse;
|
_enableMouse = enableMouse;
|
||||||
|
|
||||||
@@ -177,6 +191,79 @@ namespace Ryujinx.Input.HLE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryGetKeyboardFallback(InputConfig inputConfig, out StandardKeyboardInputConfig fallbackConfig)
|
||||||
|
{
|
||||||
|
fallbackConfig = null;
|
||||||
|
|
||||||
|
ReadOnlySpan<string> keyboardIds = _keyboardDriver.GamepadsIds;
|
||||||
|
|
||||||
|
if (keyboardIds.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string keyboardId = keyboardIds[0];
|
||||||
|
|
||||||
|
using IGamepad keyboard = _keyboardDriver.GetGamepad(keyboardId);
|
||||||
|
|
||||||
|
if (keyboard == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackConfig = new StandardKeyboardInputConfig
|
||||||
|
{
|
||||||
|
Version = InputConfig.CurrentVersion,
|
||||||
|
Backend = InputBackendType.WindowKeyboard,
|
||||||
|
Id = keyboardId,
|
||||||
|
Name = keyboard.Name,
|
||||||
|
PlayerIndex = inputConfig.PlayerIndex,
|
||||||
|
ControllerType = inputConfig.ControllerType,
|
||||||
|
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||||
|
{
|
||||||
|
DpadUp = PhysicalKey.Up,
|
||||||
|
DpadDown = PhysicalKey.Down,
|
||||||
|
DpadLeft = PhysicalKey.Left,
|
||||||
|
DpadRight = PhysicalKey.Right,
|
||||||
|
ButtonMinus = PhysicalKey.Minus,
|
||||||
|
ButtonL = PhysicalKey.E,
|
||||||
|
ButtonZl = PhysicalKey.Q,
|
||||||
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
|
},
|
||||||
|
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
|
{
|
||||||
|
StickUp = PhysicalKey.W,
|
||||||
|
StickDown = PhysicalKey.S,
|
||||||
|
StickLeft = PhysicalKey.A,
|
||||||
|
StickRight = PhysicalKey.D,
|
||||||
|
StickButton = PhysicalKey.F,
|
||||||
|
},
|
||||||
|
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||||
|
{
|
||||||
|
ButtonA = PhysicalKey.Z,
|
||||||
|
ButtonB = PhysicalKey.X,
|
||||||
|
ButtonX = PhysicalKey.C,
|
||||||
|
ButtonY = PhysicalKey.V,
|
||||||
|
ButtonPlus = PhysicalKey.Plus,
|
||||||
|
ButtonR = PhysicalKey.U,
|
||||||
|
ButtonZr = PhysicalKey.O,
|
||||||
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
|
},
|
||||||
|
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
|
{
|
||||||
|
StickUp = PhysicalKey.I,
|
||||||
|
StickDown = PhysicalKey.K,
|
||||||
|
StickLeft = PhysicalKey.J,
|
||||||
|
StickRight = PhysicalKey.L,
|
||||||
|
StickButton = PhysicalKey.H,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void UnblockInputUpdates()
|
public void UnblockInputUpdates()
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
@@ -334,7 +421,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal InputConfig GetPlayerInputConfigByIndex(int index)
|
public InputConfig GetPlayerInputConfigByIndex(int index)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Input
|
namespace Ryujinx.Input
|
||||||
{
|
{
|
||||||
@@ -33,15 +34,26 @@ namespace Ryujinx.Input
|
|||||||
{
|
{
|
||||||
if (_keyState is null)
|
if (_keyState is null)
|
||||||
{
|
{
|
||||||
_keyState = new bool[(int)Key.Count];
|
_keyState = new bool[(int)ConfigPhysicalKey.Count];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Key key = 0; key < Key.Count; key++)
|
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
|
||||||
{
|
{
|
||||||
_keyState[(int)key] = keyboard.IsPressed(key);
|
_keyState[(int)key] = keyboard.IsPressed((Key)(int)key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new KeyboardStateSnapshot(_keyState);
|
return new KeyboardStateSnapshot(_keyState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to consume a recently pressed key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The pressed key, if available.</param>
|
||||||
|
/// <returns>True if a key press was consumed.</returns>
|
||||||
|
bool TryConsumePressedKey(out Key key)
|
||||||
|
{
|
||||||
|
key = Key.Unknown;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/Ryujinx.Input/IKeyboardModeDriver.cs
Normal file
7
src/Ryujinx.Input/IKeyboardModeDriver.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Ryujinx.Input
|
||||||
|
{
|
||||||
|
public interface IKeyboardModeDriver : IGamepadDriver
|
||||||
|
{
|
||||||
|
IKeyboard GetKeyboard(string id, KeyboardInputMode mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/Ryujinx.Input/KeyboardInputMappingHelper.cs
Normal file
78
src/Ryujinx.Input/KeyboardInputMappingHelper.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
|
namespace Ryujinx.Input
|
||||||
|
{
|
||||||
|
public static class KeyboardInputMappingHelper
|
||||||
|
{
|
||||||
|
public readonly record struct KeyboardButtonMapping(GamepadButtonInputId To, ConfigPhysicalKey From)
|
||||||
|
{
|
||||||
|
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KeyboardButtonMapping[] BuildButtonMappings(StandardKeyboardInputConfig configuration) =>
|
||||||
|
[
|
||||||
|
// Left JoyCon
|
||||||
|
new(GamepadButtonInputId.LeftStick, configuration.LeftJoyconStick.StickButton),
|
||||||
|
new(GamepadButtonInputId.DpadUp, configuration.LeftJoycon.DpadUp),
|
||||||
|
new(GamepadButtonInputId.DpadDown, configuration.LeftJoycon.DpadDown),
|
||||||
|
new(GamepadButtonInputId.DpadLeft, configuration.LeftJoycon.DpadLeft),
|
||||||
|
new(GamepadButtonInputId.DpadRight, configuration.LeftJoycon.DpadRight),
|
||||||
|
new(GamepadButtonInputId.Minus, configuration.LeftJoycon.ButtonMinus),
|
||||||
|
new(GamepadButtonInputId.LeftShoulder, configuration.LeftJoycon.ButtonL),
|
||||||
|
new(GamepadButtonInputId.LeftTrigger, configuration.LeftJoycon.ButtonZl),
|
||||||
|
new(GamepadButtonInputId.SingleRightTrigger0, configuration.LeftJoycon.ButtonSr),
|
||||||
|
new(GamepadButtonInputId.SingleLeftTrigger0, configuration.LeftJoycon.ButtonSl),
|
||||||
|
|
||||||
|
// Right JoyCon
|
||||||
|
new(GamepadButtonInputId.RightStick, configuration.RightJoyconStick.StickButton),
|
||||||
|
new(GamepadButtonInputId.A, configuration.RightJoycon.ButtonA),
|
||||||
|
new(GamepadButtonInputId.B, configuration.RightJoycon.ButtonB),
|
||||||
|
new(GamepadButtonInputId.X, configuration.RightJoycon.ButtonX),
|
||||||
|
new(GamepadButtonInputId.Y, configuration.RightJoycon.ButtonY),
|
||||||
|
new(GamepadButtonInputId.Plus, configuration.RightJoycon.ButtonPlus),
|
||||||
|
new(GamepadButtonInputId.RightShoulder, configuration.RightJoycon.ButtonR),
|
||||||
|
new(GamepadButtonInputId.RightTrigger, configuration.RightJoycon.ButtonZr),
|
||||||
|
new(GamepadButtonInputId.SingleRightTrigger1, configuration.RightJoycon.ButtonSr),
|
||||||
|
new(GamepadButtonInputId.SingleLeftTrigger1, configuration.RightJoycon.ButtonSl),
|
||||||
|
];
|
||||||
|
|
||||||
|
public static (short X, short Y) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigPhysicalKey> stickConfig)
|
||||||
|
{
|
||||||
|
short stickX = 0;
|
||||||
|
short stickY = 0;
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(stickConfig.StickUp))
|
||||||
|
{
|
||||||
|
stickY += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(stickConfig.StickDown))
|
||||||
|
{
|
||||||
|
stickY -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(stickConfig.StickRight))
|
||||||
|
{
|
||||||
|
stickX += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(stickConfig.StickLeft))
|
||||||
|
{
|
||||||
|
stickX -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stickX == 0 && stickY == 0)
|
||||||
|
{
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
|
||||||
|
|
||||||
|
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/Ryujinx.Input/KeyboardInputMode.cs
Normal file
8
src/Ryujinx.Input/KeyboardInputMode.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Ryujinx.Input
|
||||||
|
{
|
||||||
|
public enum KeyboardInputMode
|
||||||
|
{
|
||||||
|
Semantic,
|
||||||
|
Physical,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Input
|
namespace Ryujinx.Input
|
||||||
{
|
{
|
||||||
@@ -25,5 +26,8 @@ namespace Ryujinx.Input
|
|||||||
/// <returns>True if the given key is pressed</returns>
|
/// <returns>True if the given key is pressed</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool IsPressed(Key key) => KeysState[(int)key];
|
public bool IsPressed(Key key) => KeysState[(int)key];
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool IsPressed(ConfigPhysicalKey key) => KeysState[(int)key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ using System.Text.Json;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
||||||
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Headless
|
namespace Ryujinx.Headless
|
||||||
{
|
{
|
||||||
@@ -105,48 +105,48 @@ namespace Ryujinx.Headless
|
|||||||
Backend = InputBackendType.WindowKeyboard,
|
Backend = InputBackendType.WindowKeyboard,
|
||||||
Id = null,
|
Id = null,
|
||||||
ControllerType = ControllerType.JoyconPair,
|
ControllerType = ControllerType.JoyconPair,
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
DpadUp = Key.Up,
|
DpadUp = PhysicalKey.Up,
|
||||||
DpadDown = Key.Down,
|
DpadDown = PhysicalKey.Down,
|
||||||
DpadLeft = Key.Left,
|
DpadLeft = PhysicalKey.Left,
|
||||||
DpadRight = Key.Right,
|
DpadRight = PhysicalKey.Right,
|
||||||
ButtonMinus = Key.Minus,
|
ButtonMinus = PhysicalKey.Minus,
|
||||||
ButtonL = Key.E,
|
ButtonL = PhysicalKey.E,
|
||||||
ButtonZl = Key.Q,
|
ButtonZl = PhysicalKey.Q,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
|
|
||||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.W,
|
StickUp = PhysicalKey.W,
|
||||||
StickDown = Key.S,
|
StickDown = PhysicalKey.S,
|
||||||
StickLeft = Key.A,
|
StickLeft = PhysicalKey.A,
|
||||||
StickRight = Key.D,
|
StickRight = PhysicalKey.D,
|
||||||
StickButton = Key.F,
|
StickButton = PhysicalKey.F,
|
||||||
},
|
},
|
||||||
|
|
||||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
ButtonA = Key.Z,
|
ButtonA = PhysicalKey.Z,
|
||||||
ButtonB = Key.X,
|
ButtonB = PhysicalKey.X,
|
||||||
ButtonX = Key.C,
|
ButtonX = PhysicalKey.C,
|
||||||
ButtonY = Key.V,
|
ButtonY = PhysicalKey.V,
|
||||||
ButtonPlus = Key.Plus,
|
ButtonPlus = PhysicalKey.Plus,
|
||||||
ButtonR = Key.U,
|
ButtonR = PhysicalKey.U,
|
||||||
ButtonZr = Key.O,
|
ButtonZr = PhysicalKey.O,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
|
|
||||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.I,
|
StickUp = PhysicalKey.I,
|
||||||
StickDown = Key.K,
|
StickDown = PhysicalKey.K,
|
||||||
StickLeft = Key.J,
|
StickLeft = PhysicalKey.J,
|
||||||
StickRight = Key.L,
|
StickRight = PhysicalKey.L,
|
||||||
StickButton = Key.H,
|
StickButton = PhysicalKey.H,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
|
|
||||||
using Key = Ryujinx.Input.Key;
|
using Key = Ryujinx.Input.Key;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Input
|
namespace Ryujinx.Ava.Input
|
||||||
{
|
{
|
||||||
internal class AvaloniaKeyboard : IKeyboard
|
internal class AvaloniaKeyboard : IKeyboard
|
||||||
{
|
{
|
||||||
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
|
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
|
||||||
private readonly AvaloniaKeyboardDriver _driver;
|
private readonly AvaloniaKeyboardDriver _driver;
|
||||||
|
private readonly KeyboardInputMode _mode;
|
||||||
private StandardKeyboardInputConfig _configuration;
|
private StandardKeyboardInputConfig _configuration;
|
||||||
|
|
||||||
private readonly Lock _userMappingLock = new();
|
private readonly Lock _userMappingLock = new();
|
||||||
@@ -24,18 +24,12 @@ namespace Ryujinx.Ava.Input
|
|||||||
|
|
||||||
public bool IsConnected => true;
|
public bool IsConnected => true;
|
||||||
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
|
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
|
||||||
|
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name, KeyboardInputMode mode)
|
||||||
private class ButtonMappingEntry(GamepadButtonInputId to, Key from)
|
|
||||||
{
|
|
||||||
public readonly GamepadButtonInputId To = to;
|
|
||||||
public readonly Key From = from;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name)
|
|
||||||
{
|
{
|
||||||
_buttonsUserMapping = [];
|
_buttonsUserMapping = [];
|
||||||
|
|
||||||
_driver = driver;
|
_driver = driver;
|
||||||
|
_mode = mode;
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
}
|
}
|
||||||
@@ -57,22 +51,18 @@ namespace Ryujinx.Ava.Input
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
|
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
|
||||||
{
|
{
|
||||||
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
|
if (!entry.IsValid || result.IsPressed(entry.To))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Do not touch state of the button already pressed.
|
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
|
||||||
if (!result.IsPressed(entry.To))
|
|
||||||
{
|
|
||||||
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
||||||
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
||||||
|
|
||||||
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
||||||
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
||||||
@@ -100,7 +90,7 @@ namespace Ryujinx.Ava.Input
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _driver.IsPressed(key);
|
return _driver.IsPressed(key, _mode);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -108,6 +98,19 @@ namespace Ryujinx.Ava.Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryConsumePressedKey(out Key key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _driver.TryConsumePressedKey(_mode, out key);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
key = Key.Unknown;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void SetConfiguration(InputConfig configuration)
|
public void SetConfiguration(InputConfig configuration)
|
||||||
{
|
{
|
||||||
lock (_userMappingLock)
|
lock (_userMappingLock)
|
||||||
@@ -116,37 +119,13 @@ namespace Ryujinx.Ava.Input
|
|||||||
|
|
||||||
_buttonsUserMapping.Clear();
|
_buttonsUserMapping.Clear();
|
||||||
|
|
||||||
#pragma warning disable IDE0055 // Disable formatting
|
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
|
||||||
// Left JoyCon
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
|
|
||||||
|
|
||||||
// Right JoyCon
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
|
|
||||||
#pragma warning restore IDE0055
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetLed(uint packedRgb)
|
public void SetLed(uint packedRgb)
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
|
Logger.Debug?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTriggerThreshold(float triggerThreshold) { }
|
public void SetTriggerThreshold(float triggerThreshold) { }
|
||||||
@@ -162,41 +141,9 @@ namespace Ryujinx.Ava.Input
|
|||||||
return value * ConvertRate;
|
return value * ConvertRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
|
|
||||||
{
|
|
||||||
short stickX = 0;
|
|
||||||
short stickY = 0;
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickUp))
|
|
||||||
{
|
|
||||||
stickY += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickDown))
|
|
||||||
{
|
|
||||||
stickY -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickRight))
|
|
||||||
{
|
|
||||||
stickX += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
|
|
||||||
{
|
|
||||||
stickX -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector2 stick = new(stickX, stickY);
|
|
||||||
|
|
||||||
stick = Vector2.Normalize(stick);
|
|
||||||
|
|
||||||
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_driver?.Clear();
|
_driver?.Clear(_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
|
|||||||
@@ -1,19 +1,37 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AvaKey = Avalonia.Input.Key;
|
using System.Threading;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
using Key = Ryujinx.Input.Key;
|
using Key = Ryujinx.Input.Key;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Input
|
namespace Ryujinx.Ava.Input
|
||||||
{
|
{
|
||||||
internal class AvaloniaKeyboardDriver : IGamepadDriver
|
internal class AvaloniaKeyboardDriver : IKeyboardModeDriver
|
||||||
{
|
{
|
||||||
|
private enum PhysicalKeySource
|
||||||
|
{
|
||||||
|
Direct,
|
||||||
|
ObservedFallback,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly string[] _keyboardIdentifers = ["0"];
|
private static readonly string[] _keyboardIdentifers = ["0"];
|
||||||
private readonly Control _control;
|
private readonly Control _control;
|
||||||
private readonly HashSet<AvaKey> _pressedKeys;
|
private readonly Window _window;
|
||||||
|
private readonly HashSet<Key> _semanticPressedKeys;
|
||||||
|
private readonly HashSet<ConfigPhysicalKey> _physicalPressedKeys;
|
||||||
|
private readonly Dictionary<Key, ConfigPhysicalKey> _observedPhysicalKeysBySemanticKey;
|
||||||
|
private readonly Queue<Key> _semanticPressedKeyQueue;
|
||||||
|
private readonly Queue<Key> _physicalPressedKeyQueue;
|
||||||
|
private readonly Lock _pressedKeyQueueLock;
|
||||||
|
private readonly KeyboardInputMode _defaultMode;
|
||||||
|
|
||||||
public event EventHandler<KeyEventArgs> KeyPressed;
|
public event EventHandler<KeyEventArgs> KeyPressed;
|
||||||
public event EventHandler<KeyEventArgs> KeyRelease;
|
public event EventHandler<KeyEventArgs> KeyRelease;
|
||||||
@@ -22,14 +40,30 @@ namespace Ryujinx.Ava.Input
|
|||||||
public string DriverName => "AvaloniaKeyboardDriver";
|
public string DriverName => "AvaloniaKeyboardDriver";
|
||||||
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
|
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
|
||||||
|
|
||||||
public AvaloniaKeyboardDriver(Control control)
|
public AvaloniaKeyboardDriver(Control control, KeyboardInputMode defaultMode = KeyboardInputMode.Semantic)
|
||||||
{
|
{
|
||||||
_control = control;
|
_control = control;
|
||||||
_pressedKeys = [];
|
_window = control as Window ?? TopLevel.GetTopLevel(control) as Window;
|
||||||
|
_semanticPressedKeys = [];
|
||||||
|
_physicalPressedKeys = [];
|
||||||
|
_observedPhysicalKeysBySemanticKey = [];
|
||||||
|
_semanticPressedKeyQueue = [];
|
||||||
|
_physicalPressedKeyQueue = [];
|
||||||
|
_pressedKeyQueueLock = new();
|
||||||
|
_defaultMode = defaultMode;
|
||||||
|
|
||||||
_control.KeyDown += OnKeyPress;
|
// Use routed handlers so keys consumed earlier in the Avalonia pipeline
|
||||||
_control.KeyUp += OnKeyRelease;
|
// can still be observed by the input driver. This is needed for keys like
|
||||||
|
// Caps Lock on macOS, which may not reach the plain CLR event path.
|
||||||
|
_control.AddHandler(InputElement.KeyDownEvent, OnKeyPress, RoutingStrategies.Tunnel, true);
|
||||||
|
_control.AddHandler(InputElement.KeyUpEvent, OnKeyRelease, RoutingStrategies.Tunnel, true);
|
||||||
_control.TextInput += Control_TextInput;
|
_control.TextInput += Control_TextInput;
|
||||||
|
_window?.Deactivated += Window_Deactivated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Window_Deactivated(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Control_TextInput(object sender, TextInputEventArgs e)
|
private void Control_TextInput(object sender, TextInputEventArgs e)
|
||||||
@@ -50,13 +84,18 @@ namespace Ryujinx.Ava.Input
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IGamepad GetGamepad(string id)
|
public IGamepad GetGamepad(string id)
|
||||||
|
{
|
||||||
|
return GetKeyboard(id, _defaultMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IKeyboard GetKeyboard(string id, KeyboardInputMode mode)
|
||||||
{
|
{
|
||||||
if (!_keyboardIdentifers[0].Equals(id))
|
if (!_keyboardIdentifers[0].Equals(id))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]);
|
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.KeyboardLayout_KeyboardInputMode], mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
|
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
|
||||||
@@ -65,40 +104,189 @@ namespace Ryujinx.Ava.Input
|
|||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_control.KeyUp -= OnKeyPress;
|
_control.RemoveHandler(InputElement.KeyDownEvent, OnKeyPress);
|
||||||
_control.KeyDown -= OnKeyRelease;
|
_control.RemoveHandler(InputElement.KeyUpEvent, OnKeyRelease);
|
||||||
|
_control.TextInput -= Control_TextInput;
|
||||||
|
if (_window != null)
|
||||||
|
{
|
||||||
|
_window.Deactivated -= Window_Deactivated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void OnKeyPress(object sender, KeyEventArgs args)
|
protected void OnKeyPress(object sender, KeyEventArgs args)
|
||||||
{
|
{
|
||||||
_pressedKeys.Add(args.Key);
|
UpdateKeyStates(args, isPressed: true);
|
||||||
|
|
||||||
KeyPressed?.Invoke(this, args);
|
KeyPressed?.Invoke(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void OnKeyRelease(object sender, KeyEventArgs args)
|
protected void OnKeyRelease(object sender, KeyEventArgs args)
|
||||||
{
|
{
|
||||||
_pressedKeys.Remove(args.Key);
|
UpdateKeyStates(args, isPressed: false);
|
||||||
|
|
||||||
KeyRelease?.Invoke(this, args);
|
KeyRelease?.Invoke(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool IsPressed(Key key)
|
internal bool IsPressed(Key key, KeyboardInputMode mode)
|
||||||
{
|
{
|
||||||
if (key is Key.Unbound or Key.Unknown)
|
if (key is Key.Unbound or Key.Unknown)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AvaloniaKeyboardMappingHelper.TryGetAvaKey(key, out AvaKey nativeKey);
|
return mode == KeyboardInputMode.Physical
|
||||||
|
? _physicalPressedKeys.Contains((ConfigPhysicalKey)(int)key)
|
||||||
|
: _semanticPressedKeys.Contains(key);
|
||||||
|
}
|
||||||
|
|
||||||
return _pressedKeys.Contains(nativeKey);
|
internal void Clear(KeyboardInputMode mode)
|
||||||
|
{
|
||||||
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
if (mode == KeyboardInputMode.Physical)
|
||||||
|
{
|
||||||
|
_physicalPressedKeys.Clear();
|
||||||
|
_physicalPressedKeyQueue.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_semanticPressedKeys.Clear();
|
||||||
|
_semanticPressedKeyQueue.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_pressedKeys.Clear();
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
_semanticPressedKeys.Clear();
|
||||||
|
_physicalPressedKeys.Clear();
|
||||||
|
_semanticPressedKeyQueue.Clear();
|
||||||
|
_physicalPressedKeyQueue.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryConsumePressedKey(KeyboardInputMode mode, out Key key)
|
||||||
|
{
|
||||||
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
Queue<Key> queue = mode == KeyboardInputMode.Physical ? _physicalPressedKeyQueue : _semanticPressedKeyQueue;
|
||||||
|
|
||||||
|
if (queue.TryDequeue(out key))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key = Key.Unknown;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateKeyState(HashSet<Key> pressedKeys, Key key, bool isPressed)
|
||||||
|
{
|
||||||
|
if (key is Key.Unknown or Key.Unbound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressed)
|
||||||
|
{
|
||||||
|
pressedKeys.Add(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pressedKeys.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateKeyState(HashSet<ConfigPhysicalKey> pressedKeys, ConfigPhysicalKey key, bool isPressed)
|
||||||
|
{
|
||||||
|
if (key is ConfigPhysicalKey.Unknown or ConfigPhysicalKey.Unbound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressed)
|
||||||
|
{
|
||||||
|
pressedKeys.Add(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pressedKeys.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateKeyStates(KeyEventArgs args, bool isPressed)
|
||||||
|
{
|
||||||
|
Key semanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.Key);
|
||||||
|
Key resolvedSemanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey, args.Key);
|
||||||
|
ConfigPhysicalKey physicalKey = GetPhysicalInputKey(args, semanticKey, out PhysicalKeySource physicalKeySource);
|
||||||
|
bool semanticWasPressed = _semanticPressedKeys.Contains(resolvedSemanticKey);
|
||||||
|
bool physicalWasPressed = _physicalPressedKeys.Contains(physicalKey);
|
||||||
|
bool semanticStateChanged = resolvedSemanticKey is not Key.Unknown and not Key.Unbound && semanticWasPressed != isPressed;
|
||||||
|
bool physicalStateChanged = physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound && physicalWasPressed != isPressed;
|
||||||
|
bool bufferedSemanticPress = false;
|
||||||
|
bool bufferedPhysicalPress = false;
|
||||||
|
|
||||||
|
UpdateKeyState(_semanticPressedKeys, resolvedSemanticKey, isPressed);
|
||||||
|
UpdateKeyState(_physicalPressedKeys, physicalKey, isPressed);
|
||||||
|
|
||||||
|
if (isPressed)
|
||||||
|
{
|
||||||
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
if (!semanticWasPressed && resolvedSemanticKey is not Key.Unknown and not Key.Unbound)
|
||||||
|
{
|
||||||
|
_semanticPressedKeyQueue.Enqueue(resolvedSemanticKey);
|
||||||
|
bufferedSemanticPress = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!physicalWasPressed && physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
|
||||||
|
{
|
||||||
|
_physicalPressedKeyQueue.Enqueue((Key)(int)physicalKey);
|
||||||
|
bufferedPhysicalPress = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressed &&
|
||||||
|
semanticKey is not Key.Unknown and not Key.Unbound &&
|
||||||
|
physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
|
||||||
|
{
|
||||||
|
_observedPhysicalKeysBySemanticKey[semanticKey] = physicalKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ConfigurationState.Instance.Logger.EnableAvaloniaLog &&
|
||||||
|
(semanticStateChanged || physicalStateChanged))
|
||||||
|
{
|
||||||
|
Logger.Info?.Print(
|
||||||
|
LogClass.UI,
|
||||||
|
$"Keyboard {(isPressed ? "down" : "up")}: avaloniaKey={args.Key}, avaloniaPhysical={args.PhysicalKey}, keySymbol={FormatKeySymbol(args.KeySymbol)}, modifiers={args.KeyModifiers}, semantic={semanticKey}, resolvedSemantic={resolvedSemanticKey}, physical={physicalKey}, physicalSource={physicalKeySource}, bufferedSemantic={bufferedSemanticPress}, bufferedPhysical={bufferedPhysicalPress}, semanticPressed={_semanticPressedKeys.Count}, physicalPressed={_physicalPressedKeys.Count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigPhysicalKey GetPhysicalInputKey(KeyEventArgs args, Key semanticKey, out PhysicalKeySource source)
|
||||||
|
{
|
||||||
|
Key key = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey);
|
||||||
|
|
||||||
|
if (key is >= Key.Unknown and < Key.Count)
|
||||||
|
{
|
||||||
|
source = PhysicalKeySource.Direct;
|
||||||
|
return (ConfigPhysicalKey)(int)key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semanticKey is not Key.Unknown and not Key.Unbound &&
|
||||||
|
_observedPhysicalKeysBySemanticKey.TryGetValue(semanticKey, out ConfigPhysicalKey observedPhysicalKey))
|
||||||
|
{
|
||||||
|
source = PhysicalKeySource.ObservedFallback;
|
||||||
|
return observedPhysicalKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
source = PhysicalKeySource.Unknown;
|
||||||
|
return ConfigPhysicalKey.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatKeySymbol(string keySymbol)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(keySymbol) ? "<none>" : keySymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Ryujinx.Input;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AvaKey = Avalonia.Input.Key;
|
using AvaKey = Avalonia.Input.Key;
|
||||||
|
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Input
|
namespace Ryujinx.Ava.Input
|
||||||
{
|
{
|
||||||
@@ -132,7 +133,8 @@ namespace Ryujinx.Ava.Input
|
|||||||
AvaKey.D8,
|
AvaKey.D8,
|
||||||
AvaKey.D9,
|
AvaKey.D9,
|
||||||
AvaKey.OemTilde,
|
AvaKey.OemTilde,
|
||||||
AvaKey.OemTilde,AvaKey.OemMinus,
|
AvaKey.Oem102,
|
||||||
|
AvaKey.OemMinus,
|
||||||
AvaKey.OemPlus,
|
AvaKey.OemPlus,
|
||||||
AvaKey.OemOpenBrackets,
|
AvaKey.OemOpenBrackets,
|
||||||
AvaKey.OemCloseBrackets,
|
AvaKey.OemCloseBrackets,
|
||||||
@@ -147,7 +149,149 @@ namespace Ryujinx.Ava.Input
|
|||||||
AvaKey.None
|
AvaKey.None
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static readonly AvaPhysicalKey[] _physicalKeyMapping =
|
||||||
|
[
|
||||||
|
// NOTE: Invalid
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
|
||||||
|
AvaPhysicalKey.ShiftLeft,
|
||||||
|
AvaPhysicalKey.ShiftRight,
|
||||||
|
AvaPhysicalKey.ControlLeft,
|
||||||
|
AvaPhysicalKey.ControlRight,
|
||||||
|
AvaPhysicalKey.AltLeft,
|
||||||
|
AvaPhysicalKey.AltRight,
|
||||||
|
AvaPhysicalKey.MetaLeft,
|
||||||
|
AvaPhysicalKey.MetaRight,
|
||||||
|
AvaPhysicalKey.ContextMenu,
|
||||||
|
AvaPhysicalKey.F1,
|
||||||
|
AvaPhysicalKey.F2,
|
||||||
|
AvaPhysicalKey.F3,
|
||||||
|
AvaPhysicalKey.F4,
|
||||||
|
AvaPhysicalKey.F5,
|
||||||
|
AvaPhysicalKey.F6,
|
||||||
|
AvaPhysicalKey.F7,
|
||||||
|
AvaPhysicalKey.F8,
|
||||||
|
AvaPhysicalKey.F9,
|
||||||
|
AvaPhysicalKey.F10,
|
||||||
|
AvaPhysicalKey.F11,
|
||||||
|
AvaPhysicalKey.F12,
|
||||||
|
AvaPhysicalKey.F13,
|
||||||
|
AvaPhysicalKey.F14,
|
||||||
|
AvaPhysicalKey.F15,
|
||||||
|
AvaPhysicalKey.F16,
|
||||||
|
AvaPhysicalKey.F17,
|
||||||
|
AvaPhysicalKey.F18,
|
||||||
|
AvaPhysicalKey.F19,
|
||||||
|
AvaPhysicalKey.F20,
|
||||||
|
AvaPhysicalKey.F21,
|
||||||
|
AvaPhysicalKey.F22,
|
||||||
|
AvaPhysicalKey.F23,
|
||||||
|
AvaPhysicalKey.F24,
|
||||||
|
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
|
||||||
|
AvaPhysicalKey.ArrowUp,
|
||||||
|
AvaPhysicalKey.ArrowDown,
|
||||||
|
AvaPhysicalKey.ArrowLeft,
|
||||||
|
AvaPhysicalKey.ArrowRight,
|
||||||
|
AvaPhysicalKey.Enter,
|
||||||
|
AvaPhysicalKey.Escape,
|
||||||
|
AvaPhysicalKey.Space,
|
||||||
|
AvaPhysicalKey.Tab,
|
||||||
|
AvaPhysicalKey.Backspace,
|
||||||
|
AvaPhysicalKey.Insert,
|
||||||
|
AvaPhysicalKey.Delete,
|
||||||
|
AvaPhysicalKey.PageUp,
|
||||||
|
AvaPhysicalKey.PageDown,
|
||||||
|
AvaPhysicalKey.Home,
|
||||||
|
AvaPhysicalKey.End,
|
||||||
|
AvaPhysicalKey.CapsLock,
|
||||||
|
AvaPhysicalKey.ScrollLock,
|
||||||
|
AvaPhysicalKey.PrintScreen,
|
||||||
|
AvaPhysicalKey.Pause,
|
||||||
|
AvaPhysicalKey.NumLock,
|
||||||
|
AvaPhysicalKey.NumPadClear,
|
||||||
|
AvaPhysicalKey.NumPad0,
|
||||||
|
AvaPhysicalKey.NumPad1,
|
||||||
|
AvaPhysicalKey.NumPad2,
|
||||||
|
AvaPhysicalKey.NumPad3,
|
||||||
|
AvaPhysicalKey.NumPad4,
|
||||||
|
AvaPhysicalKey.NumPad5,
|
||||||
|
AvaPhysicalKey.NumPad6,
|
||||||
|
AvaPhysicalKey.NumPad7,
|
||||||
|
AvaPhysicalKey.NumPad8,
|
||||||
|
AvaPhysicalKey.NumPad9,
|
||||||
|
AvaPhysicalKey.NumPadDivide,
|
||||||
|
AvaPhysicalKey.NumPadMultiply,
|
||||||
|
AvaPhysicalKey.NumPadSubtract,
|
||||||
|
AvaPhysicalKey.NumPadAdd,
|
||||||
|
AvaPhysicalKey.NumPadDecimal,
|
||||||
|
AvaPhysicalKey.NumPadEnter,
|
||||||
|
AvaPhysicalKey.A,
|
||||||
|
AvaPhysicalKey.B,
|
||||||
|
AvaPhysicalKey.C,
|
||||||
|
AvaPhysicalKey.D,
|
||||||
|
AvaPhysicalKey.E,
|
||||||
|
AvaPhysicalKey.F,
|
||||||
|
AvaPhysicalKey.G,
|
||||||
|
AvaPhysicalKey.H,
|
||||||
|
AvaPhysicalKey.I,
|
||||||
|
AvaPhysicalKey.J,
|
||||||
|
AvaPhysicalKey.K,
|
||||||
|
AvaPhysicalKey.L,
|
||||||
|
AvaPhysicalKey.M,
|
||||||
|
AvaPhysicalKey.N,
|
||||||
|
AvaPhysicalKey.O,
|
||||||
|
AvaPhysicalKey.P,
|
||||||
|
AvaPhysicalKey.Q,
|
||||||
|
AvaPhysicalKey.R,
|
||||||
|
AvaPhysicalKey.S,
|
||||||
|
AvaPhysicalKey.T,
|
||||||
|
AvaPhysicalKey.U,
|
||||||
|
AvaPhysicalKey.V,
|
||||||
|
AvaPhysicalKey.W,
|
||||||
|
AvaPhysicalKey.X,
|
||||||
|
AvaPhysicalKey.Y,
|
||||||
|
AvaPhysicalKey.Z,
|
||||||
|
AvaPhysicalKey.Digit0,
|
||||||
|
AvaPhysicalKey.Digit1,
|
||||||
|
AvaPhysicalKey.Digit2,
|
||||||
|
AvaPhysicalKey.Digit3,
|
||||||
|
AvaPhysicalKey.Digit4,
|
||||||
|
AvaPhysicalKey.Digit5,
|
||||||
|
AvaPhysicalKey.Digit6,
|
||||||
|
AvaPhysicalKey.Digit7,
|
||||||
|
AvaPhysicalKey.Digit8,
|
||||||
|
AvaPhysicalKey.Digit9,
|
||||||
|
AvaPhysicalKey.Backquote,
|
||||||
|
AvaPhysicalKey.IntlBackslash,
|
||||||
|
AvaPhysicalKey.Minus,
|
||||||
|
AvaPhysicalKey.Equal,
|
||||||
|
AvaPhysicalKey.BracketLeft,
|
||||||
|
AvaPhysicalKey.BracketRight,
|
||||||
|
AvaPhysicalKey.Semicolon,
|
||||||
|
AvaPhysicalKey.Quote,
|
||||||
|
AvaPhysicalKey.Comma,
|
||||||
|
AvaPhysicalKey.Period,
|
||||||
|
AvaPhysicalKey.Slash,
|
||||||
|
AvaPhysicalKey.Backslash,
|
||||||
|
|
||||||
|
// NOTE: invalid
|
||||||
|
AvaPhysicalKey.None
|
||||||
|
];
|
||||||
|
|
||||||
private static readonly Dictionary<AvaKey, Key> _avaKeyMapping;
|
private static readonly Dictionary<AvaKey, Key> _avaKeyMapping;
|
||||||
|
private static readonly Dictionary<AvaPhysicalKey, Key> _avaPhysicalKeyMapping;
|
||||||
|
|
||||||
static AvaloniaKeyboardMappingHelper()
|
static AvaloniaKeyboardMappingHelper()
|
||||||
{
|
{
|
||||||
@@ -155,21 +299,42 @@ namespace Ryujinx.Ava.Input
|
|||||||
|
|
||||||
// NOTE: Avalonia.Input.Key is not contiguous and quite large, so use a dictionary instead of an array.
|
// NOTE: Avalonia.Input.Key is not contiguous and quite large, so use a dictionary instead of an array.
|
||||||
_avaKeyMapping = new Dictionary<AvaKey, Key>();
|
_avaKeyMapping = new Dictionary<AvaKey, Key>();
|
||||||
|
_avaPhysicalKeyMapping = new Dictionary<AvaPhysicalKey, Key>();
|
||||||
|
|
||||||
foreach (Key key in inputKeys)
|
foreach (Key key in inputKeys)
|
||||||
{
|
{
|
||||||
if (TryGetAvaKey(key, out AvaKey index))
|
if (TryGetAvaKey(key, out AvaKey avaKey))
|
||||||
{
|
{
|
||||||
_avaKeyMapping[index] = key;
|
_avaKeyMapping[avaKey] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetAvaPhysicalKey(key, out AvaPhysicalKey avaPhysicalKey))
|
||||||
|
{
|
||||||
|
_avaPhysicalKeyMapping[avaPhysicalKey] = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alias additional Avalonia key values to improve non-US layout support.
|
||||||
|
_avaKeyMapping[AvaKey.Oem1] = Key.Semicolon;
|
||||||
|
_avaKeyMapping[AvaKey.Oem2] = Key.Slash;
|
||||||
|
_avaKeyMapping[AvaKey.Oem3] = Key.Tilde;
|
||||||
|
_avaKeyMapping[AvaKey.Oem4] = Key.BracketLeft;
|
||||||
|
_avaKeyMapping[AvaKey.Oem5] = Key.BackSlash;
|
||||||
|
_avaKeyMapping[AvaKey.Oem6] = Key.BracketRight;
|
||||||
|
_avaKeyMapping[AvaKey.Oem7] = Key.Quote;
|
||||||
|
_avaKeyMapping[AvaKey.OemBackslash] = Key.Grave;
|
||||||
|
_avaKeyMapping[AvaKey.Oem102] = Key.Grave;
|
||||||
|
|
||||||
|
// Common alternates for non-US/JIS physical keys.
|
||||||
|
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlRo] = Key.BackSlash;
|
||||||
|
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlYen] = Key.BackSlash;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetAvaKey(Key key, out AvaKey avaKey)
|
public static bool TryGetAvaKey(Key key, out AvaKey avaKey)
|
||||||
{
|
{
|
||||||
avaKey = AvaKey.None;
|
avaKey = AvaKey.None;
|
||||||
|
|
||||||
bool keyExist = (int)key < _keyMapping.Length;
|
bool keyExist = key < Key.Count && (int)key < _keyMapping.Length;
|
||||||
if (keyExist)
|
if (keyExist)
|
||||||
{
|
{
|
||||||
avaKey = _keyMapping[(int)key];
|
avaKey = _keyMapping[(int)key];
|
||||||
@@ -178,9 +343,34 @@ namespace Ryujinx.Ava.Input
|
|||||||
return keyExist;
|
return keyExist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryGetAvaPhysicalKey(Key key, out AvaPhysicalKey avaPhysicalKey)
|
||||||
|
{
|
||||||
|
avaPhysicalKey = AvaPhysicalKey.None;
|
||||||
|
|
||||||
|
bool keyExist = key < Key.Count && (int)key < _physicalKeyMapping.Length;
|
||||||
|
if (keyExist)
|
||||||
|
{
|
||||||
|
avaPhysicalKey = _physicalKeyMapping[(int)key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyExist;
|
||||||
|
}
|
||||||
|
|
||||||
public static Key ToInputKey(AvaKey key)
|
public static Key ToInputKey(AvaKey key)
|
||||||
{
|
{
|
||||||
return _avaKeyMapping.GetValueOrDefault(key, Key.Unknown);
|
return _avaKeyMapping.GetValueOrDefault(key, Key.Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Key ToInputKey(AvaPhysicalKey key)
|
||||||
|
{
|
||||||
|
return _avaPhysicalKeyMapping.GetValueOrDefault(key, Key.Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Key ToInputKey(AvaPhysicalKey physicalKey, AvaKey key)
|
||||||
|
{
|
||||||
|
Key inputKey = ToInputKey(key);
|
||||||
|
|
||||||
|
return inputKey != Key.Unknown ? inputKey : ToInputKey(physicalKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ namespace Ryujinx.Ava
|
|||||||
public static bool PreviewerDetached { get; private set; }
|
public static bool PreviewerDetached { get; private set; }
|
||||||
public static bool UseHardwareAcceleration { get; private set; }
|
public static bool UseHardwareAcceleration { get; private set; }
|
||||||
public static string BackendThreadingArg { get; private set; }
|
public static string BackendThreadingArg { get; private set; }
|
||||||
|
public static bool CoreDumpArg { get; private set; }
|
||||||
|
|
||||||
private const uint MbIconwarning = 0x30;
|
private const uint MbIconwarning = 0x30;
|
||||||
|
|
||||||
@@ -81,6 +82,8 @@ namespace Ryujinx.Ava
|
|||||||
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
|
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
|
||||||
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
|
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
|
||||||
|
|
||||||
|
CoreDumpArg = coreDumpArg;
|
||||||
|
|
||||||
// TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception.
|
// TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception.
|
||||||
// This is undesirable and causes very odd behavior during development (the process stops responding,
|
// This is undesirable and causes very odd behavior during development (the process stops responding,
|
||||||
// the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user.
|
// the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user.
|
||||||
|
|||||||
@@ -29,11 +29,6 @@
|
|||||||
<TrimMode>partial</TrimMode>
|
<TrimMode>partial</TrimMode>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-arm64'">
|
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
|
||||||
<PublishTrimmed>false</PublishTrimmed>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json.
|
FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json.
|
||||||
See:
|
See:
|
||||||
@@ -54,7 +49,7 @@
|
|||||||
<PackageReference Include="Svg.Controls.Avalonia" />
|
<PackageReference Include="Svg.Controls.Avalonia" />
|
||||||
<PackageReference Include="Svg.Controls.Skia.Avalonia" />
|
<PackageReference Include="Svg.Controls.Skia.Avalonia" />
|
||||||
<PackageReference Include="DynamicData" />
|
<PackageReference Include="DynamicData" />
|
||||||
<PackageReference Include="FluentAvaloniaUI.NoAnim" />
|
<PackageReference Include="FluentAvaloniaUI" />
|
||||||
<PackageReference Include="CommandLineParser" />
|
<PackageReference Include="CommandLineParser" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||||
<PackageReference Include="DiscordRichPresence" />
|
<PackageReference Include="DiscordRichPresence" />
|
||||||
|
|||||||
@@ -1234,9 +1234,17 @@ namespace Ryujinx.Ava.Systems
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasModalFocusLoss = _viewModel.Window is MainWindow mainWindow &&
|
||||||
|
mainWindow.SettingsWindow?.IsActive == true;
|
||||||
|
|
||||||
|
if (!_viewModel.IsActive || hasModalFocusLoss)
|
||||||
|
{
|
||||||
|
_inputManager.KeyboardDriver.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
||||||
|
|
||||||
if (_viewModel.IsActive)
|
if (_viewModel.IsActive && !hasModalFocusLoss)
|
||||||
{
|
{
|
||||||
bool isCursorVisible = true;
|
bool isCursorVisible = true;
|
||||||
|
|
||||||
@@ -1369,7 +1377,7 @@ namespace Ryujinx.Ava.Systems
|
|||||||
// Touchscreen.
|
// Touchscreen.
|
||||||
bool hasTouch = false;
|
bool hasTouch = false;
|
||||||
|
|
||||||
if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse.Value)
|
if (_viewModel.IsActive && !hasModalFocusLoss && !ConfigurationState.Instance.Hid.EnableMouse.Value)
|
||||||
{
|
{
|
||||||
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1404,7 +1404,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(data.Name))
|
if (string.IsNullOrWhiteSpace(data.Name))
|
||||||
{
|
{
|
||||||
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
|
foreach (ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
|
||||||
{
|
{
|
||||||
if (!controlTitle.NameString.IsEmpty())
|
if (!controlTitle.NameString.IsEmpty())
|
||||||
{
|
{
|
||||||
@@ -1417,7 +1417,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(data.Developer))
|
if (string.IsNullOrWhiteSpace(data.Developer))
|
||||||
{
|
{
|
||||||
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
|
foreach (ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
|
||||||
{
|
{
|
||||||
if (!controlTitle.PublisherString.IsEmpty())
|
if (!controlTitle.PublisherString.IsEmpty())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||||
|
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Systems.Configuration
|
namespace Ryujinx.Ava.Systems.Configuration
|
||||||
@@ -269,45 +271,45 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||||||
Id = "0",
|
Id = "0",
|
||||||
PlayerIndex = PlayerIndex.Player1,
|
PlayerIndex = PlayerIndex.Player1,
|
||||||
ControllerType = ControllerType.ProController,
|
ControllerType = ControllerType.ProController,
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
DpadUp = Key.Up,
|
DpadUp = PhysicalKey.Up,
|
||||||
DpadDown = Key.Down,
|
DpadDown = PhysicalKey.Down,
|
||||||
DpadLeft = Key.Left,
|
DpadLeft = PhysicalKey.Left,
|
||||||
DpadRight = Key.Right,
|
DpadRight = PhysicalKey.Right,
|
||||||
ButtonMinus = Key.Minus,
|
ButtonMinus = PhysicalKey.Minus,
|
||||||
ButtonL = Key.E,
|
ButtonL = PhysicalKey.E,
|
||||||
ButtonZl = Key.Q,
|
ButtonZl = PhysicalKey.Q,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.W,
|
StickUp = PhysicalKey.W,
|
||||||
StickDown = Key.S,
|
StickDown = PhysicalKey.S,
|
||||||
StickLeft = Key.A,
|
StickLeft = PhysicalKey.A,
|
||||||
StickRight = Key.D,
|
StickRight = PhysicalKey.D,
|
||||||
StickButton = Key.F,
|
StickButton = PhysicalKey.F,
|
||||||
},
|
},
|
||||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
ButtonA = Key.Z,
|
ButtonA = PhysicalKey.Z,
|
||||||
ButtonB = Key.X,
|
ButtonB = PhysicalKey.X,
|
||||||
ButtonX = Key.C,
|
ButtonX = PhysicalKey.C,
|
||||||
ButtonY = Key.V,
|
ButtonY = PhysicalKey.V,
|
||||||
ButtonPlus = Key.Plus,
|
ButtonPlus = PhysicalKey.Plus,
|
||||||
ButtonR = Key.U,
|
ButtonR = PhysicalKey.U,
|
||||||
ButtonZr = Key.O,
|
ButtonZr = PhysicalKey.O,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.I,
|
StickUp = PhysicalKey.I,
|
||||||
StickDown = Key.K,
|
StickDown = PhysicalKey.K,
|
||||||
StickLeft = Key.J,
|
StickLeft = PhysicalKey.J,
|
||||||
StickRight = Key.L,
|
StickRight = PhysicalKey.L,
|
||||||
StickButton = Key.H,
|
StickButton = PhysicalKey.H,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ using Ryujinx.Graphics.Vulkan;
|
|||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||||
|
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Systems.Configuration
|
namespace Ryujinx.Ava.Systems.Configuration
|
||||||
{
|
{
|
||||||
@@ -285,45 +287,45 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||||||
Name = "Keyboard",
|
Name = "Keyboard",
|
||||||
PlayerIndex = PlayerIndex.Player1,
|
PlayerIndex = PlayerIndex.Player1,
|
||||||
ControllerType = ControllerType.ProController,
|
ControllerType = ControllerType.ProController,
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
DpadUp = Key.Up,
|
DpadUp = PhysicalKey.Up,
|
||||||
DpadDown = Key.Down,
|
DpadDown = PhysicalKey.Down,
|
||||||
DpadLeft = Key.Left,
|
DpadLeft = PhysicalKey.Left,
|
||||||
DpadRight = Key.Right,
|
DpadRight = PhysicalKey.Right,
|
||||||
ButtonMinus = Key.Minus,
|
ButtonMinus = PhysicalKey.Minus,
|
||||||
ButtonL = Key.E,
|
ButtonL = PhysicalKey.E,
|
||||||
ButtonZl = Key.Q,
|
ButtonZl = PhysicalKey.Q,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.W,
|
StickUp = PhysicalKey.W,
|
||||||
StickDown = Key.S,
|
StickDown = PhysicalKey.S,
|
||||||
StickLeft = Key.A,
|
StickLeft = PhysicalKey.A,
|
||||||
StickRight = Key.D,
|
StickRight = PhysicalKey.D,
|
||||||
StickButton = Key.F,
|
StickButton = PhysicalKey.F,
|
||||||
},
|
},
|
||||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
ButtonA = Key.Z,
|
ButtonA = PhysicalKey.Z,
|
||||||
ButtonB = Key.X,
|
ButtonB = PhysicalKey.X,
|
||||||
ButtonX = Key.C,
|
ButtonX = PhysicalKey.C,
|
||||||
ButtonY = Key.V,
|
ButtonY = PhysicalKey.V,
|
||||||
ButtonPlus = Key.Plus,
|
ButtonPlus = PhysicalKey.Plus,
|
||||||
ButtonR = Key.U,
|
ButtonR = PhysicalKey.U,
|
||||||
ButtonZr = Key.O,
|
ButtonZr = PhysicalKey.O,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.I,
|
StickUp = PhysicalKey.I,
|
||||||
StickDown = Key.K,
|
StickDown = PhysicalKey.K,
|
||||||
StickLeft = Key.J,
|
StickLeft = PhysicalKey.J,
|
||||||
StickRight = Key.L,
|
StickRight = PhysicalKey.L,
|
||||||
StickButton = Key.H,
|
StickButton = PhysicalKey.H,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ namespace Ryujinx.Ava.Systems.Configuration.System
|
|||||||
SimplifiedChinese,
|
SimplifiedChinese,
|
||||||
TraditionalChinese,
|
TraditionalChinese,
|
||||||
BrazilianPortuguese,
|
BrazilianPortuguese,
|
||||||
|
Polish,
|
||||||
|
Thai,
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class LanguageEnumHelper
|
public static class LanguageEnumHelper
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
|
|
||||||
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e)
|
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
|
||||||
|
|
||||||
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
|
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||||
{
|
{
|
||||||
@@ -85,7 +85,7 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
|
|
||||||
private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e)
|
private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
|
||||||
|
|
||||||
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
|
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using Ryujinx.Ava.Input;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
using Ryujinx.Input.Assigner;
|
using Ryujinx.Input.Assigner;
|
||||||
using System;
|
using System;
|
||||||
@@ -25,6 +26,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
|
|
||||||
private bool _isWaitingForInput;
|
private bool _isWaitingForInput;
|
||||||
private bool _shouldUnbind;
|
private bool _shouldUnbind;
|
||||||
|
private IKeyboard _keyboard;
|
||||||
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
|
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
|
||||||
|
|
||||||
public ButtonKeyAssigner(ToggleButton toggleButton)
|
public ButtonKeyAssigner(ToggleButton toggleButton)
|
||||||
@@ -34,6 +36,9 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
|
|
||||||
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
|
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
|
||||||
{
|
{
|
||||||
|
_keyboard = keyboard;
|
||||||
|
ClearKeyboardState(_keyboard);
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
ToggledButton.IsChecked = true;
|
ToggledButton.IsChecked = true;
|
||||||
@@ -82,6 +87,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
_isWaitingForInput = false;
|
_isWaitingForInput = false;
|
||||||
|
|
||||||
ToggledButton.IsChecked = false;
|
ToggledButton.IsChecked = false;
|
||||||
|
ClearKeyboardState(_keyboard);
|
||||||
|
|
||||||
if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace)
|
if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace)
|
||||||
{
|
{
|
||||||
@@ -98,6 +104,15 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
_isWaitingForInput = false;
|
_isWaitingForInput = false;
|
||||||
ToggledButton.IsChecked = false;
|
ToggledButton.IsChecked = false;
|
||||||
_shouldUnbind = shouldUnbind;
|
_shouldUnbind = shouldUnbind;
|
||||||
|
ClearKeyboardState(_keyboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClearKeyboardState(IKeyboard keyboard)
|
||||||
|
{
|
||||||
|
if (keyboard is AvaloniaKeyboard avaloniaKeyboard)
|
||||||
|
{
|
||||||
|
avaloniaKeyboard.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using Key = Ryujinx.Input.Key;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
{
|
{
|
||||||
@@ -12,79 +13,6 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
{
|
{
|
||||||
public static readonly KeyValueConverter Instance = new();
|
public static readonly KeyValueConverter Instance = new();
|
||||||
|
|
||||||
private static readonly Dictionary<Key, LocaleKeys> _keysMap = new()
|
|
||||||
{
|
|
||||||
{ Key.Unknown, LocaleKeys.KeyUnknown },
|
|
||||||
{ Key.ShiftLeft, LocaleKeys.KeyShiftLeft },
|
|
||||||
{ Key.ShiftRight, LocaleKeys.KeyShiftRight },
|
|
||||||
{ Key.ControlLeft, LocaleKeys.KeyControlLeft },
|
|
||||||
{ Key.ControlRight, LocaleKeys.KeyControlRight },
|
|
||||||
{ Key.AltLeft, LocaleKeys.KeyAltLeft },
|
|
||||||
{ Key.AltRight, LocaleKeys.KeyAltRight },
|
|
||||||
{ Key.WinLeft, LocaleKeys.KeyWinLeft },
|
|
||||||
{ Key.WinRight, LocaleKeys.KeyWinRight },
|
|
||||||
{ Key.Up, LocaleKeys.KeyUp },
|
|
||||||
{ Key.Down, LocaleKeys.KeyDown },
|
|
||||||
{ Key.Left, LocaleKeys.KeyLeft },
|
|
||||||
{ Key.Right, LocaleKeys.KeyRight },
|
|
||||||
{ Key.Enter, LocaleKeys.KeyEnter },
|
|
||||||
{ Key.Escape, LocaleKeys.KeyEscape },
|
|
||||||
{ Key.Space, LocaleKeys.KeySpace },
|
|
||||||
{ Key.Tab, LocaleKeys.KeyTab },
|
|
||||||
{ Key.BackSpace, LocaleKeys.KeyBackSpace },
|
|
||||||
{ Key.Insert, LocaleKeys.KeyInsert },
|
|
||||||
{ Key.Delete, LocaleKeys.KeyDelete },
|
|
||||||
{ Key.PageUp, LocaleKeys.KeyPageUp },
|
|
||||||
{ Key.PageDown, LocaleKeys.KeyPageDown },
|
|
||||||
{ Key.Home, LocaleKeys.KeyHome },
|
|
||||||
{ Key.End, LocaleKeys.KeyEnd },
|
|
||||||
{ Key.CapsLock, LocaleKeys.KeyCapsLock },
|
|
||||||
{ Key.ScrollLock, LocaleKeys.KeyScrollLock },
|
|
||||||
{ Key.PrintScreen, LocaleKeys.KeyPrintScreen },
|
|
||||||
{ Key.Pause, LocaleKeys.KeyPause },
|
|
||||||
{ Key.NumLock, LocaleKeys.KeyNumLock },
|
|
||||||
{ Key.Clear, LocaleKeys.KeyClear },
|
|
||||||
{ Key.Keypad0, LocaleKeys.KeyKeypad0 },
|
|
||||||
{ Key.Keypad1, LocaleKeys.KeyKeypad1 },
|
|
||||||
{ Key.Keypad2, LocaleKeys.KeyKeypad2 },
|
|
||||||
{ Key.Keypad3, LocaleKeys.KeyKeypad3 },
|
|
||||||
{ Key.Keypad4, LocaleKeys.KeyKeypad4 },
|
|
||||||
{ Key.Keypad5, LocaleKeys.KeyKeypad5 },
|
|
||||||
{ Key.Keypad6, LocaleKeys.KeyKeypad6 },
|
|
||||||
{ Key.Keypad7, LocaleKeys.KeyKeypad7 },
|
|
||||||
{ Key.Keypad8, LocaleKeys.KeyKeypad8 },
|
|
||||||
{ Key.Keypad9, LocaleKeys.KeyKeypad9 },
|
|
||||||
{ Key.KeypadDivide, LocaleKeys.KeyKeypadDivide },
|
|
||||||
{ Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply },
|
|
||||||
{ Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract },
|
|
||||||
{ Key.KeypadAdd, LocaleKeys.KeyKeypadAdd },
|
|
||||||
{ Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal },
|
|
||||||
{ Key.KeypadEnter, LocaleKeys.KeyKeypadEnter },
|
|
||||||
{ Key.Number0, LocaleKeys.KeyNumber0 },
|
|
||||||
{ Key.Number1, LocaleKeys.KeyNumber1 },
|
|
||||||
{ Key.Number2, LocaleKeys.KeyNumber2 },
|
|
||||||
{ Key.Number3, LocaleKeys.KeyNumber3 },
|
|
||||||
{ Key.Number4, LocaleKeys.KeyNumber4 },
|
|
||||||
{ Key.Number5, LocaleKeys.KeyNumber5 },
|
|
||||||
{ Key.Number6, LocaleKeys.KeyNumber6 },
|
|
||||||
{ Key.Number7, LocaleKeys.KeyNumber7 },
|
|
||||||
{ Key.Number8, LocaleKeys.KeyNumber8 },
|
|
||||||
{ Key.Number9, LocaleKeys.KeyNumber9 },
|
|
||||||
{ Key.Tilde, LocaleKeys.KeyTilde },
|
|
||||||
{ Key.Grave, LocaleKeys.KeyGrave },
|
|
||||||
{ Key.Minus, LocaleKeys.KeyMinus },
|
|
||||||
{ Key.Plus, LocaleKeys.KeyPlus },
|
|
||||||
{ Key.BracketLeft, LocaleKeys.KeyBracketLeft },
|
|
||||||
{ Key.BracketRight, LocaleKeys.KeyBracketRight },
|
|
||||||
{ Key.Semicolon, LocaleKeys.KeySemicolon },
|
|
||||||
{ Key.Quote, LocaleKeys.KeyQuote },
|
|
||||||
{ Key.Comma, LocaleKeys.KeyComma },
|
|
||||||
{ Key.Period, LocaleKeys.KeyPeriod },
|
|
||||||
{ Key.Slash, LocaleKeys.KeySlash },
|
|
||||||
{ Key.BackSlash, LocaleKeys.KeyBackSlash },
|
|
||||||
{ Key.Unbound, LocaleKeys.KeyUnbound },
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly Dictionary<GamepadInputId, LocaleKeys> _gamepadInputIdMap = new()
|
private static readonly Dictionary<GamepadInputId, LocaleKeys> _gamepadInputIdMap = new()
|
||||||
{
|
{
|
||||||
{ GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick },
|
{ GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick },
|
||||||
@@ -110,49 +38,38 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
|
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
|
||||||
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
|
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
|
||||||
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
|
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
|
||||||
{ GamepadInputId.Unbound, LocaleKeys.KeyUnbound},
|
{ GamepadInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
|
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
|
||||||
{
|
{
|
||||||
{ StickInputId.Left, LocaleKeys.StickLeft},
|
{ StickInputId.Left, LocaleKeys.StickLeft},
|
||||||
{ StickInputId.Right, LocaleKeys.StickRight},
|
{ StickInputId.Right, LocaleKeys.StickRight},
|
||||||
{ StickInputId.Unbound, LocaleKeys.KeyUnbound},
|
{ StickInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
|
||||||
};
|
};
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
string keyString = string.Empty;
|
string keyString = string.Empty;
|
||||||
LocaleKeys localeKey;
|
|
||||||
|
|
||||||
switch (value)
|
switch (value)
|
||||||
{
|
{
|
||||||
case Key key:
|
case Key key:
|
||||||
if (_keysMap.TryGetValue(key, out localeKey))
|
if (KeyboardLayoutLocaleHelper.TryGetSemanticLabel(key, out string localizedKeyLabel))
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsMacOS())
|
keyString = localizedKeyLabel;
|
||||||
{
|
|
||||||
localeKey = localeKey switch
|
|
||||||
{
|
|
||||||
LocaleKeys.KeyControlLeft => LocaleKeys.KeyMacControlLeft,
|
|
||||||
LocaleKeys.KeyControlRight => LocaleKeys.KeyMacControlRight,
|
|
||||||
LocaleKeys.KeyAltLeft => LocaleKeys.KeyMacAltLeft,
|
|
||||||
LocaleKeys.KeyAltRight => LocaleKeys.KeyMacAltRight,
|
|
||||||
LocaleKeys.KeyWinLeft => LocaleKeys.KeyMacWinLeft,
|
|
||||||
LocaleKeys.KeyWinRight => LocaleKeys.KeyMacWinRight,
|
|
||||||
_ => localeKey
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
keyString = LocaleManager.Instance[localeKey];
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
keyString = key.ToString();
|
keyString = key.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case PhysicalKey physicalKey:
|
||||||
|
keyString = PhysicalKeyLabelHelper.GetDisplayString(physicalKey);
|
||||||
break;
|
break;
|
||||||
case GamepadInputId gamepadInputId:
|
case GamepadInputId gamepadInputId:
|
||||||
|
LocaleKeys localeKey;
|
||||||
if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey))
|
if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey))
|
||||||
{
|
{
|
||||||
keyString = LocaleManager.Instance[localeKey];
|
keyString = LocaleManager.Instance[localeKey];
|
||||||
|
|||||||
28
src/Ryujinx/UI/Helpers/InputDeviceNameConverter.cs
Normal file
28
src/Ryujinx/UI/Helpers/InputDeviceNameConverter.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Ryujinx.Ava.UI.Models;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
internal class InputDeviceNameConverter : MarkupExtension, IValueConverter
|
||||||
|
{
|
||||||
|
public static readonly InputDeviceNameConverter Instance = new();
|
||||||
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return value is ValueTuple<DeviceType, string, string> device ? device.Item3 : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
src/Ryujinx/UI/Helpers/KeyboardLayoutLocaleHelper.cs
Normal file
142
src/Ryujinx/UI/Helpers/KeyboardLayoutLocaleHelper.cs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
using InputKey = Ryujinx.Input.Key;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
internal static class KeyboardLayoutLocaleHelper
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<InputKey, LocaleKeys> _sharedLocalizedKeysMap = new()
|
||||||
|
{
|
||||||
|
[InputKey.Unknown] = LocaleKeys.KeyboardLayout_KeyUnknown,
|
||||||
|
[InputKey.ShiftLeft] = LocaleKeys.KeyboardLayout_KeyShiftLeft,
|
||||||
|
[InputKey.ShiftRight] = LocaleKeys.KeyboardLayout_KeyShiftRight,
|
||||||
|
[InputKey.ControlLeft] = LocaleKeys.KeyboardLayout_KeyControlLeft,
|
||||||
|
[InputKey.ControlRight] = LocaleKeys.KeyboardLayout_KeyControlRight,
|
||||||
|
[InputKey.AltLeft] = LocaleKeys.KeyboardLayout_KeyAltLeft,
|
||||||
|
[InputKey.AltRight] = LocaleKeys.KeyboardLayout_KeyAltRight,
|
||||||
|
[InputKey.WinLeft] = LocaleKeys.KeyboardLayout_KeyWinLeft,
|
||||||
|
[InputKey.WinRight] = LocaleKeys.KeyboardLayout_KeyWinRight,
|
||||||
|
[InputKey.Up] = LocaleKeys.KeyboardLayout_KeyUp,
|
||||||
|
[InputKey.Down] = LocaleKeys.KeyboardLayout_KeyDown,
|
||||||
|
[InputKey.Left] = LocaleKeys.KeyboardLayout_KeyLeft,
|
||||||
|
[InputKey.Right] = LocaleKeys.KeyboardLayout_KeyRight,
|
||||||
|
[InputKey.Enter] = LocaleKeys.KeyboardLayout_KeyEnter,
|
||||||
|
[InputKey.Escape] = LocaleKeys.KeyboardLayout_KeyEscape,
|
||||||
|
[InputKey.Space] = LocaleKeys.KeyboardLayout_KeySpace,
|
||||||
|
[InputKey.Tab] = LocaleKeys.KeyboardLayout_KeyTab,
|
||||||
|
[InputKey.BackSpace] = LocaleKeys.KeyboardLayout_KeyBackSpace,
|
||||||
|
[InputKey.Insert] = LocaleKeys.KeyboardLayout_KeyInsert,
|
||||||
|
[InputKey.Delete] = LocaleKeys.KeyboardLayout_KeyDelete,
|
||||||
|
[InputKey.PageUp] = LocaleKeys.KeyboardLayout_KeyPageUp,
|
||||||
|
[InputKey.PageDown] = LocaleKeys.KeyboardLayout_KeyPageDown,
|
||||||
|
[InputKey.Home] = LocaleKeys.KeyboardLayout_KeyHome,
|
||||||
|
[InputKey.End] = LocaleKeys.KeyboardLayout_KeyEnd,
|
||||||
|
[InputKey.CapsLock] = LocaleKeys.KeyboardLayout_KeyCapsLock,
|
||||||
|
[InputKey.ScrollLock] = LocaleKeys.KeyboardLayout_KeyScrollLock,
|
||||||
|
[InputKey.PrintScreen] = LocaleKeys.KeyboardLayout_KeyPrintScreen,
|
||||||
|
[InputKey.Pause] = LocaleKeys.KeyboardLayout_KeyPause,
|
||||||
|
[InputKey.NumLock] = LocaleKeys.KeyboardLayout_KeyNumLock,
|
||||||
|
[InputKey.Clear] = LocaleKeys.KeyboardLayout_KeyClear,
|
||||||
|
[InputKey.Keypad0] = LocaleKeys.KeyboardLayout_KeyKeypad0,
|
||||||
|
[InputKey.Keypad1] = LocaleKeys.KeyboardLayout_KeyKeypad1,
|
||||||
|
[InputKey.Keypad2] = LocaleKeys.KeyboardLayout_KeyKeypad2,
|
||||||
|
[InputKey.Keypad3] = LocaleKeys.KeyboardLayout_KeyKeypad3,
|
||||||
|
[InputKey.Keypad4] = LocaleKeys.KeyboardLayout_KeyKeypad4,
|
||||||
|
[InputKey.Keypad5] = LocaleKeys.KeyboardLayout_KeyKeypad5,
|
||||||
|
[InputKey.Keypad6] = LocaleKeys.KeyboardLayout_KeyKeypad6,
|
||||||
|
[InputKey.Keypad7] = LocaleKeys.KeyboardLayout_KeyKeypad7,
|
||||||
|
[InputKey.Keypad8] = LocaleKeys.KeyboardLayout_KeyKeypad8,
|
||||||
|
[InputKey.Keypad9] = LocaleKeys.KeyboardLayout_KeyKeypad9,
|
||||||
|
[InputKey.KeypadDivide] = LocaleKeys.KeyboardLayout_KeyKeypadDivide,
|
||||||
|
[InputKey.KeypadMultiply] = LocaleKeys.KeyboardLayout_KeyKeypadMultiply,
|
||||||
|
[InputKey.KeypadSubtract] = LocaleKeys.KeyboardLayout_KeyKeypadSubtract,
|
||||||
|
[InputKey.KeypadAdd] = LocaleKeys.KeyboardLayout_KeyKeypadAdd,
|
||||||
|
[InputKey.KeypadDecimal] = LocaleKeys.KeyboardLayout_KeyKeypadDecimal,
|
||||||
|
[InputKey.KeypadEnter] = LocaleKeys.KeyboardLayout_KeyKeypadEnter,
|
||||||
|
[InputKey.Unbound] = LocaleKeys.KeyboardLayout_KeyUnbound,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<InputKey, LocaleKeys> _semanticPrintableKeysMap = new()
|
||||||
|
{
|
||||||
|
[InputKey.Number0] = LocaleKeys.KeyboardLayout_KeyNumber0,
|
||||||
|
[InputKey.Number1] = LocaleKeys.KeyboardLayout_KeyNumber1,
|
||||||
|
[InputKey.Number2] = LocaleKeys.KeyboardLayout_KeyNumber2,
|
||||||
|
[InputKey.Number3] = LocaleKeys.KeyboardLayout_KeyNumber3,
|
||||||
|
[InputKey.Number4] = LocaleKeys.KeyboardLayout_KeyNumber4,
|
||||||
|
[InputKey.Number5] = LocaleKeys.KeyboardLayout_KeyNumber5,
|
||||||
|
[InputKey.Number6] = LocaleKeys.KeyboardLayout_KeyNumber6,
|
||||||
|
[InputKey.Number7] = LocaleKeys.KeyboardLayout_KeyNumber7,
|
||||||
|
[InputKey.Number8] = LocaleKeys.KeyboardLayout_KeyNumber8,
|
||||||
|
[InputKey.Number9] = LocaleKeys.KeyboardLayout_KeyNumber9,
|
||||||
|
[InputKey.Tilde] = LocaleKeys.KeyboardLayout_KeyTilde,
|
||||||
|
[InputKey.Grave] = LocaleKeys.KeyboardLayout_KeyGrave,
|
||||||
|
[InputKey.Minus] = LocaleKeys.KeyboardLayout_KeyMinus,
|
||||||
|
[InputKey.Plus] = LocaleKeys.KeyboardLayout_KeyPlus,
|
||||||
|
[InputKey.BracketLeft] = LocaleKeys.KeyboardLayout_KeyBracketLeft,
|
||||||
|
[InputKey.BracketRight] = LocaleKeys.KeyboardLayout_KeyBracketRight,
|
||||||
|
[InputKey.Semicolon] = LocaleKeys.KeyboardLayout_KeySemicolon,
|
||||||
|
[InputKey.Quote] = LocaleKeys.KeyboardLayout_KeyQuote,
|
||||||
|
[InputKey.Comma] = LocaleKeys.KeyboardLayout_KeyComma,
|
||||||
|
[InputKey.Period] = LocaleKeys.KeyboardLayout_KeyPeriod,
|
||||||
|
[InputKey.Slash] = LocaleKeys.KeyboardLayout_KeySlash,
|
||||||
|
[InputKey.BackSlash] = LocaleKeys.KeyboardLayout_KeyBackSlash,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool TryGetSemanticLabel(InputKey key, out string label)
|
||||||
|
{
|
||||||
|
if (TryGetSemanticLocaleKey(key, out LocaleKeys localeKey))
|
||||||
|
{
|
||||||
|
label = GetLocalizedString(localeKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
label = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetPhysicalLabel(ConfigPhysicalKey key, out string label)
|
||||||
|
{
|
||||||
|
if (TryGetPhysicalLocaleKey(key, out LocaleKeys localeKey))
|
||||||
|
{
|
||||||
|
label = GetLocalizedString(localeKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
label = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetPhysicalLocaleKey(ConfigPhysicalKey key, out LocaleKeys localeKey)
|
||||||
|
{
|
||||||
|
return _sharedLocalizedKeysMap.TryGetValue((InputKey)(int)key, out localeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetSemanticLocaleKey(InputKey key, out LocaleKeys localeKey)
|
||||||
|
{
|
||||||
|
return _sharedLocalizedKeysMap.TryGetValue(key, out localeKey) ||
|
||||||
|
_semanticPrintableKeysMap.TryGetValue(key, out localeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLocalizedString(LocaleKeys localeKey)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
localeKey = localeKey switch
|
||||||
|
{
|
||||||
|
LocaleKeys.KeyboardLayout_KeyControlLeft => LocaleKeys.KeyboardLayout_KeyMacControlLeft,
|
||||||
|
LocaleKeys.KeyboardLayout_KeyControlRight => LocaleKeys.KeyboardLayout_KeyMacControlRight,
|
||||||
|
LocaleKeys.KeyboardLayout_KeyAltLeft => LocaleKeys.KeyboardLayout_KeyMacAltLeft,
|
||||||
|
LocaleKeys.KeyboardLayout_KeyAltRight => LocaleKeys.KeyboardLayout_KeyMacAltRight,
|
||||||
|
LocaleKeys.KeyboardLayout_KeyWinLeft => LocaleKeys.KeyboardLayout_KeyMacWinLeft,
|
||||||
|
LocaleKeys.KeyboardLayout_KeyWinRight => LocaleKeys.KeyboardLayout_KeyMacWinRight,
|
||||||
|
_ => localeKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return LocaleManager.Instance[localeKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
226
src/Ryujinx/UI/Helpers/PhysicalKeyLabelHelper.cs
Normal file
226
src/Ryujinx/UI/Helpers/PhysicalKeyLabelHelper.cs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
using Avalonia.Input;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Input;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
using InputKey = Ryujinx.Input.Key;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
internal static class PhysicalKeyLabelHelper
|
||||||
|
{
|
||||||
|
private const string ObservedLabelsFileName = "keyboard_layout_labels.json";
|
||||||
|
private static readonly ConcurrentDictionary<ConfigPhysicalKey, string> _observedLayoutLabels = new();
|
||||||
|
private static readonly object _observedLayoutLabelsLock = new();
|
||||||
|
private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip
|
||||||
|
};
|
||||||
|
private static bool _observedLayoutLabelsLoaded;
|
||||||
|
public static event Action LabelsChanged;
|
||||||
|
|
||||||
|
public static string GetDisplayString(ConfigPhysicalKey key)
|
||||||
|
{
|
||||||
|
EnsureObservedLayoutLabelsLoaded();
|
||||||
|
|
||||||
|
if (KeyboardLayoutLocaleHelper.TryGetPhysicalLabel(key, out string localizedLabel))
|
||||||
|
{
|
||||||
|
return localizedLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_observedLayoutLabels.TryGetValue(key, out string observedLabel))
|
||||||
|
{
|
||||||
|
return observedLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetFallbackPrintableKeyLabel(key, out string label))
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ObserveKeyPress(object sender, KeyEventArgs args)
|
||||||
|
{
|
||||||
|
EnsureObservedLayoutLabelsLoaded();
|
||||||
|
|
||||||
|
if (args.KeyModifiers != KeyModifiers.None)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputKey inputKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey);
|
||||||
|
if (!TryConvertToConfigPhysicalKey(inputKey, out ConfigPhysicalKey physicalKey) ||
|
||||||
|
KeyboardLayoutLocaleHelper.TryGetPhysicalLocaleKey(physicalKey, out _))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryNormalizeObservedPrintableLabel(args.KeySymbol, out string label))
|
||||||
|
{
|
||||||
|
if (IsCapsLockOn() && !char.IsLetter(label[0]))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_observedLayoutLabels.TryGetValue(physicalKey, out string existingLabel) && existingLabel == label)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_observedLayoutLabels[physicalKey] = label;
|
||||||
|
SaveObservedLayoutLabels();
|
||||||
|
LabelsChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureObservedLayoutLabelsLoaded()
|
||||||
|
{
|
||||||
|
if (_observedLayoutLabelsLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_observedLayoutLabelsLock)
|
||||||
|
{
|
||||||
|
if (_observedLayoutLabelsLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string labelsPath = GetObservedLabelsPath();
|
||||||
|
|
||||||
|
if (File.Exists(labelsPath))
|
||||||
|
{
|
||||||
|
Dictionary<string, string> labels = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(labelsPath), _serializerOptions);
|
||||||
|
|
||||||
|
if (labels != null)
|
||||||
|
{
|
||||||
|
foreach ((string key, string value) in labels)
|
||||||
|
{
|
||||||
|
if (Enum.TryParse(key, out ConfigPhysicalKey physicalKey) &&
|
||||||
|
!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
_observedLayoutLabels[physicalKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
_observedLayoutLabelsLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveObservedLayoutLabels()
|
||||||
|
{
|
||||||
|
lock (_observedLayoutLabelsLock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, string> labels = [];
|
||||||
|
|
||||||
|
foreach ((ConfigPhysicalKey key, string value) in _observedLayoutLabels)
|
||||||
|
{
|
||||||
|
labels[key.ToString()] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(GetObservedLabelsPath(), JsonSerializer.Serialize(labels, _serializerOptions));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetObservedLabelsPath()
|
||||||
|
{
|
||||||
|
return Path.Combine(AppDataManager.BaseDirPath, ObservedLabelsFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetFallbackPrintableKeyLabel(ConfigPhysicalKey key, out string label)
|
||||||
|
{
|
||||||
|
// The legacy enum name for the ISO extra key is misleading, so give it a distinct physical label.
|
||||||
|
if (key == ConfigPhysicalKey.Grave)
|
||||||
|
{
|
||||||
|
label = "<>";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AvaloniaKeyboardMappingHelper.TryGetAvaPhysicalKey((InputKey)(int)key, out AvaPhysicalKey avaPhysicalKey))
|
||||||
|
{
|
||||||
|
label = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
label = PhysicalKeyExtensions.ToQwertyKeySymbol(avaPhysicalKey, false);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(label) || label.Length != 1 || char.IsControl(label[0]))
|
||||||
|
{
|
||||||
|
label = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.IsLetter(label[0]))
|
||||||
|
{
|
||||||
|
label = char.ToUpperInvariant(label[0]).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCapsLockOn()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsWindows() && Console.CapsLock;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryNormalizeObservedPrintableLabel(string keySymbol, out string label)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(keySymbol) || keySymbol.Length != 1 || char.IsControl(keySymbol[0]))
|
||||||
|
{
|
||||||
|
label = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
label = char.IsLetter(keySymbol[0])
|
||||||
|
? char.ToUpperInvariant(keySymbol[0]).ToString()
|
||||||
|
: keySymbol;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConvertToConfigPhysicalKey(InputKey key, out ConfigPhysicalKey physicalKey)
|
||||||
|
{
|
||||||
|
if (key is >= InputKey.Unknown and < InputKey.Count)
|
||||||
|
{
|
||||||
|
physicalKey = (ConfigPhysicalKey)(int)key;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicalKey = ConfigPhysicalKey.Unknown;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,88 +13,88 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
public PlayerIndex PlayerIndex { get; set; }
|
public PlayerIndex PlayerIndex { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickUp { get; set; }
|
public partial PhysicalKey LeftStickUp { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickDown { get; set; }
|
public partial PhysicalKey LeftStickDown { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickLeft { get; set; }
|
public partial PhysicalKey LeftStickLeft { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickRight { get; set; }
|
public partial PhysicalKey LeftStickRight { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickButton { get; set; }
|
public partial PhysicalKey LeftStickButton { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickUp { get; set; }
|
public partial PhysicalKey RightStickUp { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickDown { get; set; }
|
public partial PhysicalKey RightStickDown { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickLeft { get; set; }
|
public partial PhysicalKey RightStickLeft { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickRight { get; set; }
|
public partial PhysicalKey RightStickRight { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickButton { get; set; }
|
public partial PhysicalKey RightStickButton { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key DpadUp { get; set; }
|
public partial PhysicalKey DpadUp { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key DpadDown { get; set; }
|
public partial PhysicalKey DpadDown { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key DpadLeft { get; set; }
|
public partial PhysicalKey DpadLeft { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key DpadRight { get; set; }
|
public partial PhysicalKey DpadRight { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonMinus { get; set; }
|
public partial PhysicalKey ButtonMinus { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonPlus { get; set; }
|
public partial PhysicalKey ButtonPlus { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonA { get; set; }
|
public partial PhysicalKey ButtonA { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonB { get; set; }
|
public partial PhysicalKey ButtonB { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonX { get; set; }
|
public partial PhysicalKey ButtonX { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonY { get; set; }
|
public partial PhysicalKey ButtonY { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonL { get; set; }
|
public partial PhysicalKey ButtonL { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonR { get; set; }
|
public partial PhysicalKey ButtonR { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonZl { get; set; }
|
public partial PhysicalKey ButtonZl { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonZr { get; set; }
|
public partial PhysicalKey ButtonZr { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftButtonSl { get; set; }
|
public partial PhysicalKey LeftButtonSl { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftButtonSr { get; set; }
|
public partial PhysicalKey LeftButtonSr { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightButtonSl { get; set; }
|
public partial PhysicalKey RightButtonSl { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightButtonSr { get; set; }
|
public partial PhysicalKey RightButtonSr { get; set; }
|
||||||
|
|
||||||
public KeyboardInputConfig(InputConfig config)
|
public KeyboardInputConfig(InputConfig config)
|
||||||
{
|
{
|
||||||
@@ -153,7 +153,7 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
Backend = InputBackendType.WindowKeyboard,
|
Backend = InputBackendType.WindowKeyboard,
|
||||||
PlayerIndex = PlayerIndex,
|
PlayerIndex = PlayerIndex,
|
||||||
ControllerType = ControllerType,
|
ControllerType = ControllerType,
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
DpadUp = DpadUp,
|
DpadUp = DpadUp,
|
||||||
DpadDown = DpadDown,
|
DpadDown = DpadDown,
|
||||||
@@ -165,7 +165,7 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
ButtonSl = LeftButtonSl,
|
ButtonSl = LeftButtonSl,
|
||||||
ButtonSr = LeftButtonSr,
|
ButtonSr = LeftButtonSr,
|
||||||
},
|
},
|
||||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
ButtonA = ButtonA,
|
ButtonA = ButtonA,
|
||||||
ButtonB = ButtonB,
|
ButtonB = ButtonB,
|
||||||
@@ -177,7 +177,7 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
ButtonR = ButtonR,
|
ButtonR = ButtonR,
|
||||||
ButtonZr = ButtonZr,
|
ButtonZr = ButtonZr,
|
||||||
},
|
},
|
||||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = LeftStickUp,
|
StickUp = LeftStickUp,
|
||||||
StickDown = LeftStickDown,
|
StickDown = LeftStickDown,
|
||||||
@@ -185,7 +185,7 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
StickLeft = LeftStickLeft,
|
StickLeft = LeftStickLeft,
|
||||||
StickButton = LeftStickButton,
|
StickButton = LeftStickButton,
|
||||||
},
|
},
|
||||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = RightStickUp,
|
StickUp = RightStickUp,
|
||||||
StickDown = RightStickDown,
|
StickDown = RightStickDown,
|
||||||
@@ -198,5 +198,37 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void NotifyKeyLabelsChanged()
|
||||||
|
{
|
||||||
|
OnPropertiesChanged(nameof(LeftStickUp),
|
||||||
|
nameof(LeftStickDown),
|
||||||
|
nameof(LeftStickLeft),
|
||||||
|
nameof(LeftStickRight),
|
||||||
|
nameof(LeftStickButton),
|
||||||
|
nameof(RightStickUp),
|
||||||
|
nameof(RightStickDown),
|
||||||
|
nameof(RightStickLeft),
|
||||||
|
nameof(RightStickRight),
|
||||||
|
nameof(RightStickButton),
|
||||||
|
nameof(DpadUp),
|
||||||
|
nameof(DpadDown),
|
||||||
|
nameof(DpadLeft),
|
||||||
|
nameof(DpadRight),
|
||||||
|
nameof(ButtonMinus),
|
||||||
|
nameof(ButtonPlus),
|
||||||
|
nameof(ButtonA),
|
||||||
|
nameof(ButtonB),
|
||||||
|
nameof(ButtonX),
|
||||||
|
nameof(ButtonY),
|
||||||
|
nameof(ButtonL),
|
||||||
|
nameof(ButtonR),
|
||||||
|
nameof(ButtonZl),
|
||||||
|
nameof(ButtonZr),
|
||||||
|
nameof(LeftButtonSl),
|
||||||
|
nameof(LeftButtonSr),
|
||||||
|
nameof(RightButtonSl),
|
||||||
|
nameof(RightButtonSr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,42 +154,42 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
{
|
{
|
||||||
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
|
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
|
if (snapshot.IsPressed(KeyboardConfig.LeftStickRight))
|
||||||
{
|
{
|
||||||
leftBuffer.Item1 += 1;
|
leftBuffer.Item1 += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
|
if (snapshot.IsPressed(KeyboardConfig.LeftStickLeft))
|
||||||
{
|
{
|
||||||
leftBuffer.Item1 -= 1;
|
leftBuffer.Item1 -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
|
if (snapshot.IsPressed(KeyboardConfig.LeftStickUp))
|
||||||
{
|
{
|
||||||
leftBuffer.Item2 += 1;
|
leftBuffer.Item2 += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
|
if (snapshot.IsPressed(KeyboardConfig.LeftStickDown))
|
||||||
{
|
{
|
||||||
leftBuffer.Item2 -= 1;
|
leftBuffer.Item2 -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
|
if (snapshot.IsPressed(KeyboardConfig.RightStickRight))
|
||||||
{
|
{
|
||||||
rightBuffer.Item1 += 1;
|
rightBuffer.Item1 += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
|
if (snapshot.IsPressed(KeyboardConfig.RightStickLeft))
|
||||||
{
|
{
|
||||||
rightBuffer.Item1 -= 1;
|
rightBuffer.Item1 -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
|
if (snapshot.IsPressed(KeyboardConfig.RightStickUp))
|
||||||
{
|
{
|
||||||
rightBuffer.Item2 += 1;
|
rightBuffer.Item2 += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
|
if (snapshot.IsPressed(KeyboardConfig.RightStickDown))
|
||||||
{
|
{
|
||||||
rightBuffer.Item2 -= 1;
|
rightBuffer.Item2 -= 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ namespace Ryujinx.Ava.UI.Renderer
|
|||||||
|
|
||||||
Content = EmbeddedWindow;
|
Content = EmbeddedWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (EmbeddedWindow != null)
|
if (EmbeddedWindow != null)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Avalonia.Markup.Xaml;
|
|||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using FluentAvalonia.Core;
|
||||||
using FluentAvalonia.UI.Windowing;
|
using FluentAvalonia.UI.Windowing;
|
||||||
using Gommon;
|
using Gommon;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
@@ -53,6 +54,9 @@ namespace Ryujinx.Ava
|
|||||||
{
|
{
|
||||||
Name = FormatTitle();
|
Name = FormatTitle();
|
||||||
|
|
||||||
|
// Disable menu animations
|
||||||
|
FAUISettings.SetAnimationsEnabledAtAppLevel(false);
|
||||||
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS())
|
||||||
|
|||||||
@@ -88,19 +88,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
public async void ShowMotionConfig()
|
public async void ShowMotionConfig()
|
||||||
{
|
{
|
||||||
await MotionInputView.Show(this);
|
await MotionInputView.Show(this);
|
||||||
ParentModel.IsModified = true;
|
ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void ShowRumbleConfig()
|
public async void ShowRumbleConfig()
|
||||||
{
|
{
|
||||||
await RumbleInputView.Show(this);
|
await RumbleInputView.Show(this);
|
||||||
ParentModel.IsModified = true;
|
ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void ShowLedConfig()
|
public async void ShowLedConfig()
|
||||||
{
|
{
|
||||||
await LedInputView.Show(this);
|
await LedInputView.Show(this);
|
||||||
ParentModel.IsModified = true;
|
ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnParentModelChanged()
|
public void OnParentModelChanged()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Svg.Skia;
|
using Avalonia.Svg.Skia;
|
||||||
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Gommon;
|
using Gommon;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
@@ -28,7 +29,7 @@ using System.Linq;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
||||||
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.ViewModels.Input
|
namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||||
{
|
{
|
||||||
@@ -42,6 +43,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
private const string KeyboardString = "keyboard";
|
private const string KeyboardString = "keyboard";
|
||||||
private const string ControllerString = "controller";
|
private const string ControllerString = "controller";
|
||||||
private readonly MainWindow _mainWindow;
|
private readonly MainWindow _mainWindow;
|
||||||
|
private Control _keyboardDriverControl;
|
||||||
|
|
||||||
private PlayerIndex _playerId;
|
private PlayerIndex _playerId;
|
||||||
private PlayerIndex _playerIdChoose;
|
private PlayerIndex _playerIdChoose;
|
||||||
@@ -65,7 +67,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
|
|
||||||
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
|
||||||
public IGamepadDriver AvaloniaKeyboardDriver { get; }
|
public IGamepadDriver AvaloniaKeyboardDriver { get; private set; }
|
||||||
|
|
||||||
public IGamepad SelectedGamepad
|
public IGamepad SelectedGamepad
|
||||||
{
|
{
|
||||||
@@ -89,7 +91,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
|
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
|
||||||
internal ObservableCollection<ControllerModel> Controllers { get; set; }
|
internal ObservableCollection<ControllerModel> Controllers { get; set; }
|
||||||
public AvaloniaList<string> ProfilesList { get; set; }
|
public AvaloniaList<string> ProfilesList { get; set; }
|
||||||
public AvaloniaList<string> DeviceList { get; set; }
|
|
||||||
|
|
||||||
public bool UseGlobalConfig;
|
public bool UseGlobalConfig;
|
||||||
|
|
||||||
@@ -99,7 +100,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
public bool IsKeyboard => !IsController;
|
public bool IsKeyboard => !IsController;
|
||||||
public bool IsRight { get; set; }
|
public bool IsRight { get; set; }
|
||||||
public bool IsLeft { get; set; }
|
public bool IsLeft { get; set; }
|
||||||
public string RevertDeviceId { get; set; }
|
|
||||||
public bool HasLed => (SelectedGamepad.Features & GamepadFeaturesFlag.Led) != 0;
|
public bool HasLed => (SelectedGamepad.Features & GamepadFeaturesFlag.Led) != 0;
|
||||||
public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense");
|
public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense");
|
||||||
|
|
||||||
@@ -163,7 +163,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
LoadDevice();
|
LoadDevice();
|
||||||
LoadProfiles();
|
LoadProfiles();
|
||||||
|
|
||||||
RevertDeviceId = Devices[Device].Id;
|
|
||||||
_isLoaded = true;
|
_isLoaded = true;
|
||||||
_isChangeTrackingActive = true;
|
_isChangeTrackingActive = true;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
@@ -175,52 +174,58 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
get => _controller;
|
get => _controller;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
MarkAsChanged();
|
int controllerIndex = value < 0 ? 0 : value;
|
||||||
|
|
||||||
_controller = value;
|
if (controllerIndex == _controller)
|
||||||
|
|
||||||
if (_controller == -1)
|
|
||||||
{
|
{
|
||||||
_controller = 0;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
|
ApplyControllerSelection(controllerIndex);
|
||||||
{
|
RefreshModifiedState();
|
||||||
ControllerType controller = Controllers[_controller].Type;
|
|
||||||
|
|
||||||
IsLeft = true;
|
|
||||||
IsRight = true;
|
|
||||||
|
|
||||||
switch (controller)
|
|
||||||
{
|
|
||||||
case ControllerType.Handheld:
|
|
||||||
ControllerImage = JoyConPairResource;
|
|
||||||
break;
|
|
||||||
case ControllerType.ProController:
|
|
||||||
ControllerImage = ProControllerResource;
|
|
||||||
break;
|
|
||||||
case ControllerType.JoyconPair:
|
|
||||||
ControllerImage = JoyConPairResource;
|
|
||||||
break;
|
|
||||||
case ControllerType.JoyconLeft:
|
|
||||||
ControllerImage = JoyConLeftResource;
|
|
||||||
IsRight = false;
|
|
||||||
break;
|
|
||||||
case ControllerType.JoyconRight:
|
|
||||||
ControllerImage = JoyConRightResource;
|
|
||||||
IsLeft = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadInputDriver();
|
|
||||||
LoadProfiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
OnPropertyChanged();
|
|
||||||
NotifyChanges();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyControllerSelection(int controllerIndex)
|
||||||
|
{
|
||||||
|
_controller = controllerIndex;
|
||||||
|
|
||||||
|
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
|
||||||
|
{
|
||||||
|
ControllerType controller = Controllers[_controller].Type;
|
||||||
|
|
||||||
|
IsLeft = true;
|
||||||
|
IsRight = true;
|
||||||
|
|
||||||
|
switch (controller)
|
||||||
|
{
|
||||||
|
case ControllerType.Handheld:
|
||||||
|
ControllerImage = JoyConPairResource;
|
||||||
|
break;
|
||||||
|
case ControllerType.ProController:
|
||||||
|
ControllerImage = ProControllerResource;
|
||||||
|
break;
|
||||||
|
case ControllerType.JoyconPair:
|
||||||
|
ControllerImage = JoyConPairResource;
|
||||||
|
break;
|
||||||
|
case ControllerType.JoyconLeft:
|
||||||
|
ControllerImage = JoyConLeftResource;
|
||||||
|
IsRight = false;
|
||||||
|
break;
|
||||||
|
case ControllerType.JoyconRight:
|
||||||
|
ControllerImage = JoyConRightResource;
|
||||||
|
IsLeft = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadInputDriver();
|
||||||
|
LoadProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(Controller));
|
||||||
|
NotifyChanges();
|
||||||
|
}
|
||||||
|
|
||||||
public string ControllerImage
|
public string ControllerImage
|
||||||
{
|
{
|
||||||
get => _controllerImage;
|
get => _controllerImage;
|
||||||
@@ -255,33 +260,83 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
get => _device;
|
get => _device;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
MarkAsChanged();
|
if (value < 0 || value >= Devices.Count)
|
||||||
|
|
||||||
_device = value < 0 ? 0 : value;
|
|
||||||
|
|
||||||
if (_device >= Devices.Count)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_device = value;
|
||||||
|
|
||||||
DeviceType selected = Devices[_device].Type;
|
DeviceType selected = Devices[_device].Type;
|
||||||
|
|
||||||
if (selected != DeviceType.None)
|
if (selected != DeviceType.None)
|
||||||
{
|
{
|
||||||
LoadControllers();
|
|
||||||
|
|
||||||
if (_isLoaded)
|
if (_isLoaded)
|
||||||
{
|
{
|
||||||
LoadConfiguration(LoadDefaultConfiguration());
|
LoadSelectedDeviceDefaults();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LoadSelectedDeviceControllers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefreshModifiedState();
|
||||||
FindPairedDeviceInConfigFile();
|
FindPairedDeviceInConfigFile();
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(SelectedDeviceItem));
|
||||||
NotifyChanges();
|
NotifyChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ResetCurrentDeviceToDefaults()
|
||||||
|
{
|
||||||
|
RefreshAvailableDevices();
|
||||||
|
|
||||||
|
if (_device <= 0 || _device >= Devices.Count || Devices[_device].Type == DeviceType.None)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadSelectedDeviceDefaults();
|
||||||
|
RefreshModifiedState();
|
||||||
|
FindPairedDeviceInConfigFile();
|
||||||
|
NotifyChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshInputDevices()
|
||||||
|
{
|
||||||
|
RefreshAvailableDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object SelectedDeviceItem
|
||||||
|
{
|
||||||
|
get => _device >= 0 && _device < Devices.Count ? Devices[_device] : null;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is not ValueTuple<DeviceType, string, string> selectedDevice)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int deviceIndex = Devices.ToList().FindIndex(device =>
|
||||||
|
device.Type == selectedDevice.Item1 &&
|
||||||
|
device.Id == selectedDevice.Item2);
|
||||||
|
|
||||||
|
if (deviceIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceIndex == _device)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Device = deviceIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public InputConfig Config { get; set; }
|
public InputConfig Config { get; set; }
|
||||||
|
|
||||||
public InputViewModel(UserControl owner, bool useGlobal = false) : this()
|
public InputViewModel(UserControl owner, bool useGlobal = false) : this()
|
||||||
@@ -290,7 +345,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
{
|
{
|
||||||
_mainWindow = RyujinxApp.MainWindow;
|
_mainWindow = RyujinxApp.MainWindow;
|
||||||
|
|
||||||
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner);
|
ReplaceKeyboardDriver(owner);
|
||||||
|
PhysicalKeyLabelHelper.LabelsChanged += OnPhysicalKeyLabelsChanged;
|
||||||
|
|
||||||
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
||||||
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
||||||
@@ -301,7 +357,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
|
|
||||||
_isLoaded = false;
|
_isLoaded = false;
|
||||||
|
|
||||||
LoadDevices();
|
RefreshAvailableDevices();
|
||||||
|
|
||||||
PlayerId = PlayerIndex.Player1;
|
PlayerId = PlayerIndex.Player1;
|
||||||
}
|
}
|
||||||
@@ -309,13 +365,22 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
_isChangeTrackingActive = true;
|
_isChangeTrackingActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RetargetKeyboardDriver(Control owner)
|
||||||
|
{
|
||||||
|
if (!Program.PreviewerDetached)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplaceKeyboardDriver(owner);
|
||||||
|
}
|
||||||
|
|
||||||
public InputViewModel()
|
public InputViewModel()
|
||||||
{
|
{
|
||||||
PlayerIndexes = [];
|
PlayerIndexes = [];
|
||||||
Controllers = [];
|
Controllers = [];
|
||||||
Devices = [];
|
Devices = [];
|
||||||
ProfilesList = [];
|
ProfilesList = [];
|
||||||
DeviceList = [];
|
|
||||||
VisualStick = new StickVisualizer(this);
|
VisualStick = new StickVisualizer(this);
|
||||||
|
|
||||||
ControllerImage = ProControllerResource;
|
ControllerImage = ProControllerResource;
|
||||||
@@ -333,17 +398,20 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void LoadConfiguration(InputConfig inputConfig = null)
|
private InputConfig GetPersistedInputConfig()
|
||||||
{
|
{
|
||||||
if (UseGlobalConfig && Program.UseExtraConfig)
|
if (UseGlobalConfig && Program.UseExtraConfig)
|
||||||
{
|
{
|
||||||
Config = inputConfig ?? ConfigurationState.InstanceExtra.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
|
return ConfigurationState.InstanceExtra.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Config = inputConfig ?? ConfigurationState.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ConfigurationState.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadConfiguration(InputConfig inputConfig = null)
|
||||||
|
{
|
||||||
|
Config = inputConfig ?? GetDisplayedInputConfig(GetPersistedInputConfig());
|
||||||
|
|
||||||
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
|
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
|
||||||
{
|
{
|
||||||
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick);
|
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick);
|
||||||
@@ -355,6 +423,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private InputConfig GetDisplayedInputConfig(InputConfig persistedConfig)
|
||||||
|
{
|
||||||
|
if (persistedConfig is not StandardControllerInputConfig)
|
||||||
|
{
|
||||||
|
return persistedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputConfig activeConfig = _mainWindow?.ViewModel.AppHost?.NpadManager?.GetPlayerInputConfigByIndex((int)_playerId);
|
||||||
|
|
||||||
|
return activeConfig is StandardKeyboardInputConfig ? activeConfig : persistedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
private void FindPairedDeviceInConfigFile()
|
private void FindPairedDeviceInConfigFile()
|
||||||
{
|
{
|
||||||
// This function allows you to output a message about the device configuration found in the file
|
// This function allows you to output a message about the device configuration found in the file
|
||||||
@@ -375,16 +455,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MarkAsChanged()
|
|
||||||
{
|
|
||||||
//If tracking is active, then allow changing the modifier
|
|
||||||
if (!IsModified && _isChangeTrackingActive)
|
|
||||||
{
|
|
||||||
RevertDeviceId = Devices[Device].Id; // Remember the device to undo changes
|
|
||||||
IsModified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnlinkDevice()
|
public void UnlinkDevice()
|
||||||
{
|
{
|
||||||
// "Disabled" mode is available after unbinding the device
|
// "Disabled" mode is available after unbinding the device
|
||||||
@@ -395,34 +465,117 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
|
|
||||||
public void LoadDevice()
|
public void LoadDevice()
|
||||||
{
|
{
|
||||||
|
int deviceIndex = 0;
|
||||||
|
|
||||||
if (Config == null || Config.Backend == InputBackendType.Invalid)
|
if (Config == null || Config.Backend == InputBackendType.Invalid)
|
||||||
{
|
{
|
||||||
Device = 0;
|
ApplyLoadedDevice(deviceIndex);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
DeviceType type = DeviceType.None;
|
||||||
|
|
||||||
|
if (Config is StandardKeyboardInputConfig)
|
||||||
{
|
{
|
||||||
DeviceType type = DeviceType.None;
|
type = DeviceType.Keyboard;
|
||||||
|
|
||||||
if (Config is StandardKeyboardInputConfig)
|
|
||||||
{
|
|
||||||
type = DeviceType.Keyboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Config is StandardControllerInputConfig)
|
|
||||||
{
|
|
||||||
type = DeviceType.Controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
(DeviceType Type, string Id, string Name) item = Devices.FirstOrDefault(x => x.Type == type && x.Id == Config.Id);
|
|
||||||
if (item != default)
|
|
||||||
{
|
|
||||||
Device = Devices.ToList().FindIndex(x => x.Id == item.Id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Device = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Config is StandardControllerInputConfig)
|
||||||
|
{
|
||||||
|
type = DeviceType.Controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
(DeviceType Type, string Id, string Name) item = Devices.FirstOrDefault(x => x.Type == type && x.Id == Config.Id);
|
||||||
|
|
||||||
|
if (item != default)
|
||||||
|
{
|
||||||
|
deviceIndex = Devices.ToList().FindIndex(x => x.Id == item.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyLoadedDevice(deviceIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLoadedDevice(int deviceIndex)
|
||||||
|
{
|
||||||
|
_device = deviceIndex is >= 0 and < int.MaxValue ? deviceIndex : 0;
|
||||||
|
|
||||||
|
if (_device >= Devices.Count)
|
||||||
|
{
|
||||||
|
_device = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_device > 0 && Devices[_device].Type != DeviceType.None)
|
||||||
|
{
|
||||||
|
LoadControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
FindPairedDeviceInConfigFile();
|
||||||
|
OnPropertyChanged(nameof(Device));
|
||||||
|
OnPropertyChanged(nameof(SelectedDeviceItem));
|
||||||
|
NotifyChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadSelectedDeviceControllers()
|
||||||
|
{
|
||||||
|
if (_device > 0 && _device < Devices.Count && Devices[_device].Type != DeviceType.None)
|
||||||
|
{
|
||||||
|
LoadControllers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadSelectedDeviceDefaults()
|
||||||
|
{
|
||||||
|
LoadSelectedDeviceControllers();
|
||||||
|
LoadConfiguration(LoadDefaultConfiguration());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshModifiedState()
|
||||||
|
{
|
||||||
|
if (!_isChangeTrackingActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsModified = !ConfigsMatch(GetSelectedDeviceConfig(), GetDisplayedInputConfig(GetPersistedInputConfig()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ConfigsMatch(InputConfig currentConfig, InputConfig otherConfig)
|
||||||
|
{
|
||||||
|
if (currentConfig == null || otherConfig == null)
|
||||||
|
{
|
||||||
|
return currentConfig == otherConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonHelper.Serialize(currentConfig, _serializerContext.InputConfig) ==
|
||||||
|
JsonHelper.Serialize(otherConfig, _serializerContext.InputConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private InputConfig GetSelectedDeviceConfig()
|
||||||
|
{
|
||||||
|
if (_device <= 0 || _device >= Devices.Count)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
(DeviceType Type, string Id, string Name) device = Devices[_device];
|
||||||
|
InputConfig config = device.Type switch
|
||||||
|
{
|
||||||
|
DeviceType.Keyboard => (ConfigViewModel as KeyboardInputViewModel)?.Config.GetConfig(),
|
||||||
|
DeviceType.Controller => (ConfigViewModel as ControllerInputViewModel)?.Config.GetConfig(),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Id = device.Type == DeviceType.Keyboard ? device.Id : device.Id.Split(" ")[0];
|
||||||
|
config.Name = device.Name;
|
||||||
|
config.PlayerIndex = _playerId;
|
||||||
|
config.ControllerType = Controllers[_controller].Type;
|
||||||
|
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadInputDriver()
|
private void LoadInputDriver()
|
||||||
@@ -462,11 +615,24 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
{
|
{
|
||||||
_isChangeTrackingActive = false; // Disable configuration change tracking
|
_isChangeTrackingActive = false; // Disable configuration change tracking
|
||||||
|
|
||||||
LoadDevices();
|
bool shouldApplyKeyboardFallback = Config is StandardControllerInputConfig controllerConfig && controllerConfig.Id == id;
|
||||||
|
|
||||||
IsModified = true;
|
RefreshAvailableDevices();
|
||||||
RevertChanges();
|
|
||||||
FindPairedDeviceInConfigFile();
|
if (shouldApplyKeyboardFallback)
|
||||||
|
{
|
||||||
|
LoadConfiguration();
|
||||||
|
LoadDevice();
|
||||||
|
NotificationIsVisible = false;
|
||||||
|
IsModified = false;
|
||||||
|
NotifyChanges();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
IsModified = true;
|
||||||
|
RevertChanges();
|
||||||
|
FindPairedDeviceInConfigFile();
|
||||||
|
}
|
||||||
|
|
||||||
_isChangeTrackingActive = true; // Enable configuration change tracking
|
_isChangeTrackingActive = true; // Enable configuration change tracking
|
||||||
|
|
||||||
@@ -476,7 +642,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
{
|
{
|
||||||
_isChangeTrackingActive = false; // Disable configuration change tracking
|
_isChangeTrackingActive = false; // Disable configuration change tracking
|
||||||
|
|
||||||
LoadDevices();
|
RefreshAvailableDevices();
|
||||||
|
|
||||||
IsModified = true;
|
IsModified = true;
|
||||||
RevertChanges();
|
RevertChanges();
|
||||||
@@ -502,6 +668,23 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
return device.Id.Split(" ")[0];
|
return device.Id.Split(" ")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetCurrentConfigDeviceId()
|
||||||
|
{
|
||||||
|
if (_device < 0 || _device >= Devices.Count)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
(DeviceType Type, string Id, string Name) device = Devices[_device];
|
||||||
|
|
||||||
|
return device.Type switch
|
||||||
|
{
|
||||||
|
DeviceType.Keyboard => device.Id,
|
||||||
|
DeviceType.Controller => device.Id.Split(" ")[0],
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public void LoadControllers()
|
public void LoadControllers()
|
||||||
{
|
{
|
||||||
Controllers.Clear();
|
Controllers.Clear();
|
||||||
@@ -510,7 +693,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
{
|
{
|
||||||
Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeHandheld]));
|
Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeHandheld]));
|
||||||
|
|
||||||
Controller = 0;
|
ApplyControllerSelection(0);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -528,14 +711,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
// Workaround: set the box to 1 and then 0
|
// Workaround: set the box to 1 and then 0
|
||||||
if (controllerIndex == 0)
|
if (controllerIndex == 0)
|
||||||
{
|
{
|
||||||
Controller = 1;
|
ApplyControllerSelection(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
Controller = controllerIndex;
|
ApplyControllerSelection(controllerIndex);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Controller = 0;
|
ApplyControllerSelection(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -561,8 +744,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
return str[(str.IndexOf(Hyphen) + Offset)..];
|
return str[(str.IndexOf(Hyphen) + Offset)..];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadDevices()
|
private void RefreshAvailableDevices()
|
||||||
{
|
{
|
||||||
|
int selectedDeviceIndex = 0;
|
||||||
|
(DeviceType Type, string Id, string Name) selectedDevice = default;
|
||||||
|
|
||||||
|
if (_device >= 0 && _device < Devices.Count)
|
||||||
|
{
|
||||||
|
selectedDevice = Devices[_device];
|
||||||
|
}
|
||||||
|
|
||||||
string GetGamepadName(IGamepad gamepad, int controllerNumber)
|
string GetGamepadName(IGamepad gamepad, int controllerNumber)
|
||||||
{
|
{
|
||||||
return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
|
return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
|
||||||
@@ -583,7 +774,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
lock (Devices)
|
lock (Devices)
|
||||||
{
|
{
|
||||||
Devices.Clear();
|
Devices.Clear();
|
||||||
DeviceList.Clear();
|
|
||||||
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
|
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
|
||||||
|
|
||||||
|
|
||||||
@@ -609,9 +799,20 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceList.AddRange(Devices.Select(x => x.Name));
|
if (selectedDevice != default)
|
||||||
Device = Math.Min(Device, DeviceList.Count);
|
{
|
||||||
|
selectedDeviceIndex = Devices.ToList().FindIndex(device =>
|
||||||
|
device.Type == selectedDevice.Type &&
|
||||||
|
device.Id == selectedDevice.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedDeviceIndex < 0)
|
||||||
|
{
|
||||||
|
selectedDeviceIndex = Math.Clamp(_device, 0, Devices.Count - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplyLoadedDevice(selectedDeviceIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetProfileBasePath()
|
private string GetProfileBasePath()
|
||||||
@@ -677,46 +878,46 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
Id = id,
|
Id = id,
|
||||||
Name = name,
|
Name = name,
|
||||||
ControllerType = ControllerType.ProController,
|
ControllerType = ControllerType.ProController,
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
DpadUp = Key.Up,
|
DpadUp = PhysicalKey.Up,
|
||||||
DpadDown = Key.Down,
|
DpadDown = PhysicalKey.Down,
|
||||||
DpadLeft = Key.Left,
|
DpadLeft = PhysicalKey.Left,
|
||||||
DpadRight = Key.Right,
|
DpadRight = PhysicalKey.Right,
|
||||||
ButtonMinus = Key.Minus,
|
ButtonMinus = PhysicalKey.Minus,
|
||||||
ButtonL = Key.E,
|
ButtonL = PhysicalKey.E,
|
||||||
ButtonZl = Key.Q,
|
ButtonZl = PhysicalKey.Q,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
LeftJoyconStick =
|
LeftJoyconStick =
|
||||||
new JoyconConfigKeyboardStick<Key>
|
new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.W,
|
StickUp = PhysicalKey.W,
|
||||||
StickDown = Key.S,
|
StickDown = PhysicalKey.S,
|
||||||
StickLeft = Key.A,
|
StickLeft = PhysicalKey.A,
|
||||||
StickRight = Key.D,
|
StickRight = PhysicalKey.D,
|
||||||
StickButton = Key.F,
|
StickButton = PhysicalKey.F,
|
||||||
},
|
},
|
||||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
ButtonA = Key.Z,
|
ButtonA = PhysicalKey.Z,
|
||||||
ButtonB = Key.X,
|
ButtonB = PhysicalKey.X,
|
||||||
ButtonX = Key.C,
|
ButtonX = PhysicalKey.C,
|
||||||
ButtonY = Key.V,
|
ButtonY = PhysicalKey.V,
|
||||||
ButtonPlus = Key.Plus,
|
ButtonPlus = PhysicalKey.Plus,
|
||||||
ButtonR = Key.U,
|
ButtonR = PhysicalKey.U,
|
||||||
ButtonZr = Key.O,
|
ButtonZr = PhysicalKey.O,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.I,
|
StickUp = PhysicalKey.I,
|
||||||
StickDown = Key.K,
|
StickDown = PhysicalKey.K,
|
||||||
StickLeft = Key.J,
|
StickLeft = PhysicalKey.J,
|
||||||
StickRight = Key.L,
|
StickRight = PhysicalKey.L,
|
||||||
StickButton = Key.H,
|
StickButton = PhysicalKey.H,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -860,7 +1061,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
{
|
{
|
||||||
_isLoaded = false;
|
_isLoaded = false;
|
||||||
|
|
||||||
config.Id = Config.Id; // Set current device id instead of changing device(independent profiles)
|
string currentDeviceId = Config?.Id ?? GetCurrentConfigDeviceId();
|
||||||
|
if (string.IsNullOrEmpty(currentDeviceId))
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Configuration, $"Ignoring profile load for {ProfileName} because no active input device is selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Id = currentDeviceId; // Set current device id instead of changing device(independent profiles)
|
||||||
|
|
||||||
LoadConfiguration(config);
|
LoadConfiguration(config);
|
||||||
|
|
||||||
@@ -958,9 +1166,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
|
|
||||||
public void RevertChanges()
|
public void RevertChanges()
|
||||||
{
|
{
|
||||||
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);
|
|
||||||
|
|
||||||
_isLoaded = false;
|
_isLoaded = false;
|
||||||
LoadConfiguration();
|
LoadConfiguration();
|
||||||
LoadDevice();
|
LoadDevice();
|
||||||
@@ -980,8 +1185,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
|
|
||||||
IsModified = false;
|
IsModified = false;
|
||||||
|
|
||||||
RevertDeviceId = Devices[Device].Id; // Remember selected device after saving
|
|
||||||
|
|
||||||
List<InputConfig> newConfig = [];
|
List<InputConfig> newConfig = [];
|
||||||
|
|
||||||
if (UseGlobalConfig && Program.UseExtraConfig)
|
if (UseGlobalConfig && Program.UseExtraConfig)
|
||||||
@@ -1001,25 +1204,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
(DeviceType Type, string Id, string Name) device = Devices[Device];
|
InputConfig config = GetSelectedDeviceConfig();
|
||||||
|
|
||||||
if (device.Type == DeviceType.Keyboard)
|
|
||||||
{
|
|
||||||
KeyboardInputConfig inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config;
|
|
||||||
inputConfig.Id = device.Id;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
GamepadInputConfig inputConfig = (ConfigViewModel as ControllerInputViewModel).Config;
|
|
||||||
inputConfig.Id = device.Id.Split(" ")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
InputConfig config = !IsController
|
|
||||||
? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig()
|
|
||||||
: (ConfigViewModel as ControllerInputViewModel).Config.GetConfig();
|
|
||||||
config.ControllerType = Controllers[_controller].Type;
|
|
||||||
config.PlayerIndex = _playerId;
|
|
||||||
config.Name = device.Name;
|
|
||||||
|
|
||||||
int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId);
|
int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId);
|
||||||
if (i == -1)
|
if (i == -1)
|
||||||
@@ -1060,9 +1245,46 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
NotifyChangesEvent?.Invoke();
|
NotifyChangesEvent?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnPhysicalKeyLabelsChanged()
|
||||||
|
{
|
||||||
|
if (ConfigViewModel is KeyboardInputViewModel keyboardInputViewModel)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(keyboardInputViewModel.Config.NotifyKeyLabelsChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReplaceKeyboardDriver(Control owner)
|
||||||
|
{
|
||||||
|
Control target = TopLevel.GetTopLevel(owner) as Control ?? owner;
|
||||||
|
|
||||||
|
if (ReferenceEquals(_keyboardDriverControl, target))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AvaloniaKeyboardDriver is AvaloniaKeyboardDriver oldKeyboardDriver)
|
||||||
|
{
|
||||||
|
oldKeyboardDriver.KeyPressed -= PhysicalKeyLabelHelper.ObserveKeyPress;
|
||||||
|
oldKeyboardDriver.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_keyboardDriverControl = target;
|
||||||
|
|
||||||
|
AvaloniaKeyboardDriver keyboardDriver = new(target, KeyboardInputMode.Physical);
|
||||||
|
keyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
|
||||||
|
AvaloniaKeyboardDriver = keyboardDriver;
|
||||||
|
|
||||||
|
if (_isLoaded && Device > 0 && Device < Devices.Count && Devices[Device].Type == DeviceType.Keyboard)
|
||||||
|
{
|
||||||
|
SelectedGamepad?.Dispose();
|
||||||
|
LoadInputDriver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
|
PhysicalKeyLabelHelper.LabelsChanged -= OnPhysicalKeyLabelsChanged;
|
||||||
|
|
||||||
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
|
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
|
||||||
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
|
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
|
||||||
@@ -1073,7 +1295,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
|
|
||||||
SelectedGamepad?.Dispose();
|
SelectedGamepad?.Dispose();
|
||||||
|
|
||||||
AvaloniaKeyboardDriver.Dispose();
|
AvaloniaKeyboardDriver?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
private string _screenshotKey = "F8";
|
private string _screenshotKey = "F8";
|
||||||
private float _volume;
|
private float _volume;
|
||||||
private ApplicationData _currentApplicationData;
|
private ApplicationData _currentApplicationData;
|
||||||
|
private bool _pendingRestart;
|
||||||
private readonly AutoResetEvent _rendererWaitEvent;
|
private readonly AutoResetEvent _rendererWaitEvent;
|
||||||
private int _customVSyncInterval;
|
private int _customVSyncInterval;
|
||||||
private int _customVSyncIntervalPercentageProxy;
|
private int _customVSyncIntervalPercentageProxy;
|
||||||
@@ -1062,7 +1063,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
string dialogMessage =
|
string dialogMessage =
|
||||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage);
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage);
|
||||||
|
|
||||||
if (ContentManager.AreKeysAlredyPresent(systemDirectory))
|
if (ContentManager.AreKeysAlreadyPresent(systemDirectory))
|
||||||
{
|
{
|
||||||
dialogMessage +=
|
dialogMessage +=
|
||||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys
|
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys
|
||||||
@@ -1250,6 +1251,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
|
|
||||||
await LoadApplication(_currentApplicationData);
|
await LoadApplication(_currentApplicationData);
|
||||||
}
|
}
|
||||||
|
else if (_pendingRestart)
|
||||||
|
{
|
||||||
|
_pendingRestart = false;
|
||||||
|
|
||||||
|
Logger.Info?.Print(LogClass.Application, $"Restarting emulation for '{_currentApplicationData.Name}'");
|
||||||
|
|
||||||
|
await LoadApplication(_currentApplicationData);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Otherwise, clear state.
|
// Otherwise, clear state.
|
||||||
@@ -1258,6 +1267,21 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RestartEmulation()
|
||||||
|
{
|
||||||
|
if (AppHost is null || _currentApplicationData is null)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.Application, "RestartEmulation called but no application is running.");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Info?.Print(LogClass.Application, $"Restart requested for '{_currentApplicationData.Name}'");
|
||||||
|
|
||||||
|
_pendingRestart = true;
|
||||||
|
AppHost.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
private void Update_StatusBar(object sender, StatusUpdatedEventArgs args)
|
private void Update_StatusBar(object sender, StatusUpdatedEventArgs args)
|
||||||
{
|
{
|
||||||
if (ShowMenuAndStatusBar && !ShowLoadProgress)
|
if (ShowMenuAndStatusBar && !ShowLoadProgress)
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
if (e.ButtonValue.HasValue)
|
if (e.ButtonValue.HasValue)
|
||||||
{
|
{
|
||||||
Button buttonValue = e.ButtonValue.Value;
|
Button buttonValue = e.ButtonValue.Value;
|
||||||
FlagInputConfigChanged();
|
|
||||||
|
|
||||||
switch (button.Name)
|
switch (button.Name)
|
||||||
{
|
{
|
||||||
@@ -187,6 +186,8 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
viewModel.Config.RightJoystick = buttonValue.AsHidType<StickInputId>();
|
viewModel.Config.RightJoystick = buttonValue.AsHidType<StickInputId>();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FlagInputConfigChanged();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,7 +213,7 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
|
|
||||||
private void FlagInputConfigChanged()
|
private void FlagInputConfigChanged()
|
||||||
{
|
{
|
||||||
(DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true;
|
(DataContext as ControllerInputViewModel)!.ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MouseClick(object sender, PointerPressedEventArgs e)
|
private void MouseClick(object sender, PointerPressedEventArgs e)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
|
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||||
@@ -148,7 +149,7 @@
|
|||||||
<Grid
|
<Grid
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="2"
|
Margin="2"
|
||||||
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto">
|
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="5,0,10,0"
|
Margin="5,0,10,0"
|
||||||
@@ -161,19 +162,38 @@
|
|||||||
Name="DeviceBox"
|
Name="DeviceBox"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
ItemsSource="{Binding DeviceList}"
|
ItemsSource="{Binding Devices}"
|
||||||
SelectedIndex="{Binding Device}" />
|
SelectedItem="{Binding SelectedDeviceItem, Mode=TwoWay}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding ., Converter={x:Static helpers:InputDeviceNameConverter.Instance}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
<Button
|
<Button
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
MinWidth="0"
|
MinWidth="0"
|
||||||
Margin="5,0,0,0"
|
Margin="5,0,0,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Command="{Binding LoadDevice}">
|
ToolTip.Tip="{ext:Locale ControllerSettingsRefresh}"
|
||||||
|
Command="{Binding RefreshInputDevices}">
|
||||||
<ui:SymbolIcon
|
<ui:SymbolIcon
|
||||||
Symbol="Refresh"
|
Symbol="Refresh"
|
||||||
FontSize="15"
|
FontSize="15"
|
||||||
Height="20"/>
|
Height="20"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
Grid.Column="3"
|
||||||
|
MinWidth="0"
|
||||||
|
Margin="5,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip.Tip="{ext:Locale ControllerSettingsResetKeybindsToDefault}"
|
||||||
|
Command="{Binding ResetCurrentDeviceToDefaults}">
|
||||||
|
<ui:SymbolIcon
|
||||||
|
Symbol="Undo"
|
||||||
|
FontSize="15"
|
||||||
|
Height="20"/>
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<!-- Controller Type -->
|
<!-- Controller Type -->
|
||||||
<Grid
|
<Grid
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Systems.Configuration;
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
@@ -15,9 +16,14 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
|
|
||||||
public InputView()
|
public InputView()
|
||||||
{
|
{
|
||||||
ViewModel = new InputViewModel(this, ConfigurationState.Instance.System.UseInputGlobalConfig);
|
ReplaceViewModel(ConfigurationState.Instance.System.UseInputGlobalConfig);
|
||||||
|
}
|
||||||
|
|
||||||
InitializeComponent();
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnAttachedToVisualTree(e);
|
||||||
|
|
||||||
|
ViewModel?.RetargetKeyboardDriver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveCurrentProfile()
|
public void SaveCurrentProfile()
|
||||||
@@ -28,8 +34,18 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
|
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
|
||||||
{
|
{
|
||||||
Dispose();
|
Dispose();
|
||||||
ViewModel = new InputViewModel(this, enableConfigGlobal); // Create new Input Page with global input configs
|
ReplaceViewModel(enableConfigGlobal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReplaceViewModel(bool useGlobalConfig)
|
||||||
|
{
|
||||||
|
ViewModel = new InputViewModel(this, useGlobalConfig); // Create new Input Page with the selected input config scope.
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
if (VisualRoot is not null)
|
||||||
|
{
|
||||||
|
ViewModel.RetargetKeyboardDriver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using Ryujinx.Input.Assigner;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Button = Ryujinx.Input.Button;
|
using Button = Ryujinx.Input.Button;
|
||||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Views.Input
|
namespace Ryujinx.Ava.UI.Views.Input
|
||||||
{
|
{
|
||||||
@@ -73,95 +73,96 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
if (be.ButtonValue.HasValue)
|
if (be.ButtonValue.HasValue)
|
||||||
{
|
{
|
||||||
Button buttonValue = be.ButtonValue.Value;
|
Button buttonValue = be.ButtonValue.Value;
|
||||||
ViewModel.ParentModel.IsModified = true;
|
|
||||||
|
|
||||||
switch (button.Name)
|
switch (button.Name)
|
||||||
{
|
{
|
||||||
case "ButtonZl":
|
case "ButtonZl":
|
||||||
ViewModel.Config.ButtonZl = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonZl = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonL":
|
case "ButtonL":
|
||||||
ViewModel.Config.ButtonL = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonL = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonMinus":
|
case "ButtonMinus":
|
||||||
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickButton":
|
case "LeftStickButton":
|
||||||
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<Key>();
|
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickUp":
|
case "LeftStickUp":
|
||||||
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<Key>();
|
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickDown":
|
case "LeftStickDown":
|
||||||
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<Key>();
|
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickRight":
|
case "LeftStickRight":
|
||||||
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<Key>();
|
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickLeft":
|
case "LeftStickLeft":
|
||||||
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<Key>();
|
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "DpadUp":
|
case "DpadUp":
|
||||||
ViewModel.Config.DpadUp = buttonValue.AsHidType<Key>();
|
ViewModel.Config.DpadUp = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "DpadDown":
|
case "DpadDown":
|
||||||
ViewModel.Config.DpadDown = buttonValue.AsHidType<Key>();
|
ViewModel.Config.DpadDown = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "DpadLeft":
|
case "DpadLeft":
|
||||||
ViewModel.Config.DpadLeft = buttonValue.AsHidType<Key>();
|
ViewModel.Config.DpadLeft = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "DpadRight":
|
case "DpadRight":
|
||||||
ViewModel.Config.DpadRight = buttonValue.AsHidType<Key>();
|
ViewModel.Config.DpadRight = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftButtonSr":
|
case "LeftButtonSr":
|
||||||
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<Key>();
|
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftButtonSl":
|
case "LeftButtonSl":
|
||||||
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<Key>();
|
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightButtonSr":
|
case "RightButtonSr":
|
||||||
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<Key>();
|
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightButtonSl":
|
case "RightButtonSl":
|
||||||
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<Key>();
|
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonZr":
|
case "ButtonZr":
|
||||||
ViewModel.Config.ButtonZr = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonZr = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonR":
|
case "ButtonR":
|
||||||
ViewModel.Config.ButtonR = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonR = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonPlus":
|
case "ButtonPlus":
|
||||||
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonA":
|
case "ButtonA":
|
||||||
ViewModel.Config.ButtonA = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonA = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonB":
|
case "ButtonB":
|
||||||
ViewModel.Config.ButtonB = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonB = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonX":
|
case "ButtonX":
|
||||||
ViewModel.Config.ButtonX = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonX = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonY":
|
case "ButtonY":
|
||||||
ViewModel.Config.ButtonY = buttonValue.AsHidType<Key>();
|
ViewModel.Config.ButtonY = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickButton":
|
case "RightStickButton":
|
||||||
ViewModel.Config.RightStickButton = buttonValue.AsHidType<Key>();
|
ViewModel.Config.RightStickButton = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickUp":
|
case "RightStickUp":
|
||||||
ViewModel.Config.RightStickUp = buttonValue.AsHidType<Key>();
|
ViewModel.Config.RightStickUp = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickDown":
|
case "RightStickDown":
|
||||||
ViewModel.Config.RightStickDown = buttonValue.AsHidType<Key>();
|
ViewModel.Config.RightStickDown = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickRight":
|
case "RightStickRight":
|
||||||
ViewModel.Config.RightStickRight = buttonValue.AsHidType<Key>();
|
ViewModel.Config.RightStickRight = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickLeft":
|
case "RightStickLeft":
|
||||||
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<Key>();
|
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ViewModel.ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -207,40 +208,40 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
{
|
{
|
||||||
Dictionary<string, Action> buttonActions = new()
|
Dictionary<string, Action> buttonActions = new()
|
||||||
{
|
{
|
||||||
{ "ButtonZl", () => ViewModel.Config.ButtonZl = Key.Unbound },
|
{ "ButtonZl", () => ViewModel.Config.ButtonZl = PhysicalKey.Unbound },
|
||||||
{ "ButtonL", () => ViewModel.Config.ButtonL = Key.Unbound },
|
{ "ButtonL", () => ViewModel.Config.ButtonL = PhysicalKey.Unbound },
|
||||||
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = Key.Unbound },
|
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = PhysicalKey.Unbound },
|
||||||
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = Key.Unbound },
|
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = PhysicalKey.Unbound },
|
||||||
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = Key.Unbound },
|
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = PhysicalKey.Unbound },
|
||||||
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = Key.Unbound },
|
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = PhysicalKey.Unbound },
|
||||||
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = Key.Unbound },
|
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = PhysicalKey.Unbound },
|
||||||
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = Key.Unbound },
|
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = PhysicalKey.Unbound },
|
||||||
{ "DpadUp", () => ViewModel.Config.DpadUp = Key.Unbound },
|
{ "DpadUp", () => ViewModel.Config.DpadUp = PhysicalKey.Unbound },
|
||||||
{ "DpadDown", () => ViewModel.Config.DpadDown = Key.Unbound },
|
{ "DpadDown", () => ViewModel.Config.DpadDown = PhysicalKey.Unbound },
|
||||||
{ "DpadLeft", () => ViewModel.Config.DpadLeft = Key.Unbound },
|
{ "DpadLeft", () => ViewModel.Config.DpadLeft = PhysicalKey.Unbound },
|
||||||
{ "DpadRight", () => ViewModel.Config.DpadRight = Key.Unbound },
|
{ "DpadRight", () => ViewModel.Config.DpadRight = PhysicalKey.Unbound },
|
||||||
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = Key.Unbound },
|
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = PhysicalKey.Unbound },
|
||||||
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = Key.Unbound },
|
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = PhysicalKey.Unbound },
|
||||||
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = Key.Unbound },
|
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = PhysicalKey.Unbound },
|
||||||
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = Key.Unbound },
|
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = PhysicalKey.Unbound },
|
||||||
{ "ButtonZr", () => ViewModel.Config.ButtonZr = Key.Unbound },
|
{ "ButtonZr", () => ViewModel.Config.ButtonZr = PhysicalKey.Unbound },
|
||||||
{ "ButtonR", () => ViewModel.Config.ButtonR = Key.Unbound },
|
{ "ButtonR", () => ViewModel.Config.ButtonR = PhysicalKey.Unbound },
|
||||||
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = Key.Unbound },
|
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = PhysicalKey.Unbound },
|
||||||
{ "ButtonA", () => ViewModel.Config.ButtonA = Key.Unbound },
|
{ "ButtonA", () => ViewModel.Config.ButtonA = PhysicalKey.Unbound },
|
||||||
{ "ButtonB", () => ViewModel.Config.ButtonB = Key.Unbound },
|
{ "ButtonB", () => ViewModel.Config.ButtonB = PhysicalKey.Unbound },
|
||||||
{ "ButtonX", () => ViewModel.Config.ButtonX = Key.Unbound },
|
{ "ButtonX", () => ViewModel.Config.ButtonX = PhysicalKey.Unbound },
|
||||||
{ "ButtonY", () => ViewModel.Config.ButtonY = Key.Unbound },
|
{ "ButtonY", () => ViewModel.Config.ButtonY = PhysicalKey.Unbound },
|
||||||
{ "RightStickButton", () => ViewModel.Config.RightStickButton = Key.Unbound },
|
{ "RightStickButton", () => ViewModel.Config.RightStickButton = PhysicalKey.Unbound },
|
||||||
{ "RightStickUp", () => ViewModel.Config.RightStickUp = Key.Unbound },
|
{ "RightStickUp", () => ViewModel.Config.RightStickUp = PhysicalKey.Unbound },
|
||||||
{ "RightStickDown", () => ViewModel.Config.RightStickDown = Key.Unbound },
|
{ "RightStickDown", () => ViewModel.Config.RightStickDown = PhysicalKey.Unbound },
|
||||||
{ "RightStickRight", () => ViewModel.Config.RightStickRight = Key.Unbound },
|
{ "RightStickRight", () => ViewModel.Config.RightStickRight = PhysicalKey.Unbound },
|
||||||
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = Key.Unbound }
|
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = PhysicalKey.Unbound }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
|
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
|
||||||
{
|
{
|
||||||
action();
|
action();
|
||||||
ViewModel.ParentModel.IsModified = true;
|
ViewModel.ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,18 +47,13 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Orientation="Horizontal" IsEnabled="{Binding ShowLedColorPicker}">
|
<StackPanel Orientation="Horizontal" IsEnabled="{Binding ShowLedColorPicker}">
|
||||||
<TextBlock MinWidth="75" MaxWidth="200" Text="{ext:Locale ControllerSettingsLedColor}" />
|
<TextBlock MinWidth="75" MaxWidth="200" Text="{ext:Locale ControllerSettingsLedColor}" />
|
||||||
<ui:ColorPickerButton
|
<ColorPicker
|
||||||
Margin="5"
|
Margin="5"
|
||||||
IsMoreButtonVisible="False"
|
|
||||||
UseColorPalette="False"
|
|
||||||
UseColorTriangle="False"
|
|
||||||
UseColorWheel="False"
|
|
||||||
ShowAcceptDismissButtons="False"
|
|
||||||
IsAlphaEnabled="False"
|
IsAlphaEnabled="False"
|
||||||
AttachedToVisualTree="ColorPickerButton_OnAttachedToVisualTree"
|
AttachedToVisualTree="ColorPicker_OnAttachedToVisualTree"
|
||||||
ColorChanged="ColorPickerButton_OnColorChanged"
|
ColorChanged="ColorPicker_OnColorChanged"
|
||||||
Color="{Binding LedColor, Mode=TwoWay}">
|
Color="{Binding LedColor, Mode=TwoWay}">
|
||||||
</ui:ColorPickerButton>
|
</ColorPicker>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.UI.Controls;
|
using Ryujinx.Ava.UI.Controls;
|
||||||
@@ -30,19 +31,17 @@ namespace Ryujinx.UI.Views.Input
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ColorPickerButton_OnColorChanged(ColorPickerButton sender, ColorButtonColorChangedEventArgs args)
|
private void ColorPicker_OnColorChanged(object sender, ColorChangedEventArgs args)
|
||||||
{
|
{
|
||||||
if (!args.NewColor.HasValue)
|
|
||||||
return;
|
|
||||||
if (!ViewModel.EnableLedChanging)
|
if (!ViewModel.EnableLedChanging)
|
||||||
return;
|
return;
|
||||||
if (ViewModel.TurnOffLed)
|
if (ViewModel.TurnOffLed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ViewModel.ParentModel.SelectedGamepad.SetLed(args.NewColor.Value.ToUInt32());
|
ViewModel.ParentModel.SelectedGamepad.SetLed(args.NewColor.ToUInt32());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ColorPickerButton_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
private void ColorPicker_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
if (!ViewModel.EnableLedChanging)
|
if (!ViewModel.EnableLedChanging)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -167,6 +167,12 @@
|
|||||||
Icon="{ext:Icon fa-solid fa-stop}"
|
Icon="{ext:Icon fa-solid fa-stop}"
|
||||||
InputGesture="Escape"
|
InputGesture="Escape"
|
||||||
IsEnabled="{Binding IsGameRunning}" />
|
IsEnabled="{Binding IsGameRunning}" />
|
||||||
|
<MenuItem
|
||||||
|
Name="RestartEmulationMenuItem"
|
||||||
|
Header="{ext:Locale MenuBarOptionsRestartEmulation}"
|
||||||
|
Icon="{ext:Icon fa-solid fa-rotate-right}"
|
||||||
|
InputGesture="Ctrl + R"
|
||||||
|
IsEnabled="{Binding IsGameRunning}" />
|
||||||
<MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{ext:Locale MenuBarOptionsSimulateWakeUpMessage}" Icon="{ext:Icon fa-solid fa-sun}" />
|
<MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{ext:Locale MenuBarOptionsSimulateWakeUpMessage}" Icon="{ext:Icon fa-solid fa-sun}" />
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||||||
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
|
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
|
||||||
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
|
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
|
||||||
StopEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
|
StopEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
|
||||||
|
RestartEmulationMenuItem.Command = Commands.Create(() => ViewModel.RestartEmulation());
|
||||||
CheatManagerMenuItem.Command = Commands.CreateSilentFail(OpenCheatManagerForCurrentApp);
|
CheatManagerMenuItem.Command = Commands.CreateSilentFail(OpenCheatManagerForCurrentApp);
|
||||||
InstallFileTypesMenuItem.Command = Commands.Create(InstallFileTypes);
|
InstallFileTypesMenuItem.Command = Commands.Create(InstallFileTypes);
|
||||||
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
|
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
|
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this, KeyboardInputMode.Semantic);
|
||||||
|
_avaloniaKeyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
<Button
|
<Button
|
||||||
Name="SaveButton"
|
Name="SaveButton"
|
||||||
Click="SaveButton_Click">
|
Click="SaveButton_Click">
|
||||||
<TextBlock Text="{ext:Locale UserProfilesSetProfileImage}" />
|
<TextBlock Text="{ext:Locale UserProfilesSave}" />
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -78,22 +78,16 @@
|
|||||||
Spacing="10"
|
Spacing="10"
|
||||||
Margin="0 24 0 0"
|
Margin="0 24 0 0"
|
||||||
HorizontalAlignment="Right">
|
HorizontalAlignment="Right">
|
||||||
<ui:ColorPickerButton
|
<ColorPicker
|
||||||
FlyoutPlacement="Top"
|
|
||||||
IsMoreButtonVisible="False"
|
|
||||||
UseColorPalette="False"
|
|
||||||
UseColorTriangle="False"
|
|
||||||
UseColorWheel="False"
|
|
||||||
ShowAcceptDismissButtons="False"
|
|
||||||
IsAlphaEnabled="False"
|
IsAlphaEnabled="False"
|
||||||
Color="{Binding BackgroundColor, Mode=TwoWay}"
|
Color="{Binding BackgroundColor, Mode=TwoWay}"
|
||||||
Name="ColorButton">
|
Name="ColorButton">
|
||||||
<ui:ColorPickerButton.Styles>
|
<ColorPicker.Styles>
|
||||||
<Style Selector="Grid#Root > DockPanel > Grid">
|
<Style Selector="Grid#Root > DockPanel > Grid">
|
||||||
<Setter Property="IsVisible" Value="False" />
|
<Setter Property="IsVisible" Value="False" />
|
||||||
</Style>
|
</Style>
|
||||||
</ui:ColorPickerButton.Styles>
|
</ColorPicker.Styles>
|
||||||
</ui:ColorPickerButton>
|
</ColorPicker>
|
||||||
<Button
|
<Button
|
||||||
Content="{ext:Locale AvatarChoose}"
|
Content="{ext:Locale AvatarChoose}"
|
||||||
Height="35"
|
Height="35"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
<KeyBinding Gesture="Escape" Command="{Binding ExitCurrentState}" />
|
<KeyBinding Gesture="Escape" Command="{Binding ExitCurrentState}" />
|
||||||
<KeyBinding Gesture="Ctrl+A" Command="{Binding OpenAmiiboWindow}" />
|
<KeyBinding Gesture="Ctrl+A" Command="{Binding OpenAmiiboWindow}" />
|
||||||
<KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" />
|
<KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" />
|
||||||
|
<KeyBinding Gesture="Ctrl+R" Command="{Binding RestartEmulation}" />
|
||||||
<KeyBinding Gesture="Ctrl+Shift+R" Command="{Binding ReloadRenderDocApi}" />
|
<KeyBinding Gesture="Ctrl+Shift+R" Command="{Binding ReloadRenderDocApi}" />
|
||||||
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding ToggleCapture}" />
|
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding ToggleCapture}" />
|
||||||
</Window.KeyBindings>
|
</Window.KeyBindings>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ using Ryujinx.HLE.HOS;
|
|||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using Ryujinx.Input.HLE;
|
using Ryujinx.Input.HLE;
|
||||||
using Ryujinx.Input.SDL3;
|
using Ryujinx.Input.SDL3;
|
||||||
|
using Ryujinx.Input;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -105,7 +106,9 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
|
|
||||||
if (Program.PreviewerDetached)
|
if (Program.PreviewerDetached)
|
||||||
{
|
{
|
||||||
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL3GamepadDriver());
|
AvaloniaKeyboardDriver keyboardDriver = new(this, KeyboardInputMode.Semantic);
|
||||||
|
keyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
|
||||||
|
InputManager = new InputManager(keyboardDriver, new SDL3GamepadDriver());
|
||||||
|
|
||||||
_ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it);
|
_ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it);
|
||||||
this.ScalingChanged += OnScalingChanged;
|
this.ScalingChanged += OnScalingChanged;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Gommon;
|
using Gommon;
|
||||||
|
using Ryujinx.Common.Utilities;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -11,29 +13,42 @@ namespace Ryujinx.Ava.Utilities
|
|||||||
extension(IStorageProvider storageProvider)
|
extension(IStorageProvider storageProvider)
|
||||||
{
|
{
|
||||||
public Task<Optional<IStorageFolder>> OpenSingleFolderPickerAsync(FolderPickerOpenOptions openOptions = null) =>
|
public Task<Optional<IStorageFolder>> OpenSingleFolderPickerAsync(FolderPickerOpenOptions openOptions = null) =>
|
||||||
storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, false))
|
CoreDumpable(() => storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, false)))
|
||||||
.Then(folders => folders.FindFirst());
|
.Then(folders => folders.FindFirst());
|
||||||
|
|
||||||
public Task<Optional<IStorageFile>> OpenSingleFilePickerAsync(FilePickerOpenOptions openOptions = null) =>
|
public Task<Optional<IStorageFile>> OpenSingleFilePickerAsync(FilePickerOpenOptions openOptions = null) =>
|
||||||
storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, false))
|
CoreDumpable(() => storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, false)))
|
||||||
.Then(files => files.FindFirst());
|
.Then(files => files.FindFirst());
|
||||||
|
|
||||||
public Task<Optional<IReadOnlyList<IStorageFolder>>> OpenMultiFolderPickerAsync(FolderPickerOpenOptions openOptions = null) =>
|
public Task<Optional<IReadOnlyList<IStorageFolder>>> OpenMultiFolderPickerAsync(FolderPickerOpenOptions openOptions = null) =>
|
||||||
storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, true))
|
CoreDumpable(() => storageProvider.OpenFolderPickerAsync(FixOpenOptions(openOptions, true)))
|
||||||
.Then(folders => folders.Count > 0 ? Optional.Of(folders) : default);
|
.Then(folders => folders.Count > 0 ? Optional.Of(folders) : default);
|
||||||
|
|
||||||
public Task<Optional<IReadOnlyList<IStorageFile>>> OpenMultiFilePickerAsync(FilePickerOpenOptions openOptions = null) =>
|
public Task<Optional<IReadOnlyList<IStorageFile>>> OpenMultiFilePickerAsync(FilePickerOpenOptions openOptions = null) =>
|
||||||
storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, true))
|
CoreDumpable(() => storageProvider.OpenFilePickerAsync(FixOpenOptions(openOptions, true)))
|
||||||
.Then(files => files.Count > 0 ? Optional.Of(files) : default);
|
.Then(files => files.Count > 0 ? Optional.Of(files) : default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<T> CoreDumpable<T>(Func<Task<T>> picker)
|
||||||
|
{
|
||||||
|
OsUtils.SetCoreDumpable(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await picker();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!Program.CoreDumpArg)
|
||||||
|
OsUtils.SetCoreDumpable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static FilePickerOpenOptions FixOpenOptions(this FilePickerOpenOptions openOptions, bool allowMultiple)
|
private static FilePickerOpenOptions FixOpenOptions(this FilePickerOpenOptions openOptions, bool allowMultiple)
|
||||||
{
|
{
|
||||||
if (openOptions is null)
|
if (openOptions is null)
|
||||||
return new FilePickerOpenOptions { AllowMultiple = allowMultiple };
|
return new FilePickerOpenOptions { AllowMultiple = allowMultiple };
|
||||||
|
|
||||||
openOptions.AllowMultiple = allowMultiple;
|
openOptions.AllowMultiple = allowMultiple;
|
||||||
|
|
||||||
return openOptions;
|
return openOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +58,6 @@ namespace Ryujinx.Ava.Utilities
|
|||||||
return new FolderPickerOpenOptions { AllowMultiple = allowMultiple };
|
return new FolderPickerOpenOptions { AllowMultiple = allowMultiple };
|
||||||
|
|
||||||
openOptions.AllowMultiple = allowMultiple;
|
openOptions.AllowMultiple = allowMultiple;
|
||||||
|
|
||||||
return openOptions;
|
return openOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Management;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ namespace Ryujinx.Ava.Utilities.SystemInfo
|
|||||||
{
|
{
|
||||||
internal WindowsSystemInfo()
|
internal WindowsSystemInfo()
|
||||||
{
|
{
|
||||||
CpuName = $"{GetCpuidCpuName() ?? GetCpuNameWMI()} ; {LogicalCoreCount} logical"; // WMI is very slow
|
CpuName = $"{GetCpuidCpuName() ?? GetCpuNameFromRegistry()} ; {LogicalCoreCount} logical";
|
||||||
(RamTotal, RamAvailable) = GetMemoryStats();
|
(RamTotal, RamAvailable) = GetMemoryStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,25 +27,26 @@ namespace Ryujinx.Ava.Utilities.SystemInfo
|
|||||||
return (0, 0);
|
return (0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetCpuNameWMI()
|
private static string GetCpuNameFromRegistry()
|
||||||
{
|
{
|
||||||
ManagementObjectCollection cpuObjs = GetWMIObjects("root\\CIMV2", "SELECT * FROM Win32_Processor");
|
try
|
||||||
|
|
||||||
if (cpuObjs != null)
|
|
||||||
{
|
{
|
||||||
foreach (ManagementBaseObject cpuObj in cpuObjs)
|
using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
|
||||||
{
|
|
||||||
return cpuObj["Name"].ToString().Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")?.Trim();
|
return key?.GetValue("ProcessorNameString")?.ToString()?.Trim();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Application, $"Registry CPU name lookup failed: {ex.Message}");
|
||||||
|
|
||||||
|
return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER")?.Trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
private struct MemoryStatusEx
|
private struct MemoryStatusEx()
|
||||||
{
|
{
|
||||||
public uint Length;
|
public uint Length = (uint)Marshal.SizeOf<MemoryStatusEx>();
|
||||||
public uint MemoryLoad;
|
public uint MemoryLoad;
|
||||||
public ulong TotalPhys;
|
public ulong TotalPhys;
|
||||||
public ulong AvailPhys;
|
public ulong AvailPhys;
|
||||||
@@ -55,33 +55,10 @@ namespace Ryujinx.Ava.Utilities.SystemInfo
|
|||||||
public ulong TotalVirtual;
|
public ulong TotalVirtual;
|
||||||
public ulong AvailVirtual;
|
public ulong AvailVirtual;
|
||||||
public ulong AvailExtendedVirtual;
|
public ulong AvailExtendedVirtual;
|
||||||
|
|
||||||
public MemoryStatusEx()
|
|
||||||
{
|
|
||||||
Length = (uint)Marshal.SizeOf<MemoryStatusEx>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static partial bool GlobalMemoryStatusEx(ref MemoryStatusEx lpBuffer);
|
private static partial bool GlobalMemoryStatusEx(ref MemoryStatusEx lpBuffer);
|
||||||
|
|
||||||
private static ManagementObjectCollection GetWMIObjects(string scope, string query)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new ManagementObjectSearcher(scope, query).Get();
|
|
||||||
}
|
|
||||||
catch (PlatformNotSupportedException ex)
|
|
||||||
{
|
|
||||||
Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
|
|
||||||
}
|
|
||||||
catch (COMException ex)
|
|
||||||
{
|
|
||||||
Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user