Compare commits

..

108 Commits

Author SHA1 Message Date
Babib3l
0a59f8d154 Clear stuck keyboard input when windows lose focus 2026-04-08 19:48:37 +02:00
Babib3l
031cd90048 Log keyboard UI events only on key state changes 2026-04-08 18:19:19 +02:00
Babib3l
8d5adfed14 Gate keyboard event logs behind the Avalonia UI log setting 2026-04-08 17:59:16 +02:00
Babib3l
5aab5f205d Merge branch ryujinx:master into keyboard-localisation-fracture-share 2026-04-02 15:54:30 +02:00
Babib3l
bfff9ff780 Merge branch ryujinx:master into keyboard-localisation-fracture-share 2026-04-01 18:38:59 +02:00
Babib3l
2a28dbde83 Add non functional reset keybinds button and refresh labels 2026-04-01 15:34:08 +02:00
Babib3l
70207cd374 Fix macOS Caps Lock capture in Avalonia keyboard driver 2026-04-01 15:14:28 +02:00
Babib3l
23b9a47d08 Refresh input modified state only when keybinds actually change 2026-03-30 22:04:23 +02:00
Babib3l
fa0696ca27 Rename and privatize input device refresh helper 2026-03-30 21:40:32 +02:00
Babib3l
087655972d Restore input device default reset behavior 2026-03-30 21:36:53 +02:00
Babib3l
25306c221d Add UI keyboard trace logs for key state and rebinding 2026-03-30 20:07:22 +02:00
_Neo_
cd1ce67f89 Improve 1 locale ID 2026-03-30 20:25:41 +03:00
_Neo_
4c38af62ae Revert "Update Locale ID's for better readability"
Reverting the "readability" updates to the locale IDs due to helpful feedback from LotP. If these go get changed, then only in a similar MR.
2026-03-30 20:22:03 +03:00
Babib3l
903cb3f22f Merge branch ryujinx:master into keyboard-localisation-fracture-share 2026-03-29 13:23:49 +02:00
Babib3l
d3f6460fdf Fix macOS Caps Lock rebinding by buffering one-shot key-down events during keyboard assignment 2026-03-24 22:45:37 +01:00
Babib3l
c1845aac4d Copilot key fix 2026-03-24 12:49:07 +01:00
Babib3l
943d69e93d Sh0inx line review Fixes 2026-03-24 12:45:25 +01:00
_Neo_
ce07a2be68 Update Locale ID's for better readability 2026-03-22 22:37:27 +02:00
_Neo_
a8c18a9853 Update all Keyboard Labels
Moves keyboard labels that were updated in the UI input MR, to this MR, due to it fitting the purpose better and to avoid double work.
2026-03-22 21:59:15 +02:00
Babib3l
c8367335d4 Fix input settings device selection state 2026-03-22 18:15:12 +01:00
Babib3l
ca015200f1 Persist observed physical key labels 2026-03-22 17:29:55 +01:00
Babib3l
5809661664 fix for caps lock 2026-03-22 12:43:52 +01:00
Babib3l
9aeb0c8c8c Fallback to keyboard on controller disconnect 2026-03-22 01:05:17 +01:00
Babib3l
d1464eb5f2 Simplify keyboard input cleanup paths 2026-03-22 00:40:45 +01:00
Babib3l
e4b920002f Fix input device refresh button 2026-03-21 23:44:39 +01:00
Babib3l
0b02e71a66 fixed KeyDown events latching gameplay keys 2026-03-21 23:32:46 +01:00
Babib3l
7becde9d8e Fix settings keyboard input focus loss 2026-03-21 22:45:19 +01:00
Babib3l
9a5e4c06af Merge branch 'keyboard-localisation-fracture-share' of https://git.ryujinx.app/babib3l/Ryujinx into keyboard-localisation-fracture-share 2026-03-21 21:33:56 +01:00
Babib3l
cd4aa41a8f Guard input profile loading when config is missing 2026-03-21 17:55:04 +01:00
_Neo_
b3d18f7845 Initial Prep for Keyboard Label Transfer
Preparing to transfer updated keyboard labels from another MR into this one.
2026-03-20 22:55:47 +02:00
Babib3l
3cbe372b18 Simplify gameplay keyboard physical-key paths 2026-03-19 20:45:48 +01:00
Babib3l
327f90b420 Refresh keyboard labels when layout changes 2026-03-19 20:25:01 +01:00
Babib3l
818399ecfc add .dotnet-home/ to .gitignore 2026-03-18 22:12:24 +01:00
Babib3l
1e5c4fedbd Fix Build error 2026-03-18 21:55:50 +01:00
Babib3l
3401c29b81 Overall Code Cleanup 2026-03-18 21:34:13 +01:00
Babib3l
39f58e453b Track keyboard labels from host layout events 2026-03-18 21:19:35 +01:00
Babib3l
84f3ce2ca5 Update keyboard localisation refactor snapshot 2026-03-18 21:10:28 +01:00
Babib3l
2fe5e8c40d Cache Avalonia keyboard LED state 2026-03-18 20:21:24 +01:00
Babib3l
ebd8cc4f4a Added better support for the different keyboards. 2026-03-18 18:17:06 +01:00
Babib3l
13c8b57063 Fix AltGr key assignment and silence keyboard SetLed logs 2026-03-18 17:13:43 +01:00
Babib3l
32f603d7ad Merge branch ryujinx:master into keyboard-localisation-fracture 2026-03-18 16:42:11 +01:00
Babib3l
7df5299d5a Split keyboard localisation into dedicated locale file 2026-03-18 16:38:31 +01:00
Babib3l
1b7ffbe723 Merge branch ryujinx:master into master 2026-03-14 22:28:08 +01:00
Babib3l
d23b2c162b Merge branch ryujinx:master into master 2026-03-02 17:36:53 +01:00
Babib3l
128e16b9d3 Merge branch ryujinx:master into master 2026-03-01 13:04:54 +01:00
Babib3l
0fff818fdf Merge branch ryujinx:master into master 2026-02-11 17:12:12 +01:00
Babib3l
5eb5eeb285 Merge branch ryujinx:master into master 2026-02-03 09:53:56 +01:00
Babib3l
aabbb3c5d2 Merge branch ryujinx:master into master 2026-01-28 14:12:30 +01:00
Babib3l
b8d5744fd3 Merge branch ryujinx:master into master 2026-01-28 13:54:25 +01:00
Babib3l
5954f8f3b7 Merge branch ryujinx:master into master 2026-01-05 22:38:20 +01:00
Babib3l3l
041c088d61 french and spanish translations 2026-01-03 13:36:36 +01:00
Babib3l
ddd9ba8aba Merge branch ryujinx:master into master 2026-01-03 13:17:06 +01:00
Babib3l
f788e1211d Merge branch ryujinx:master into master 2025-11-18 12:43:50 +01:00
Babib3l
d34aa0e549 Merge branch ryujinx:master into master 2025-11-15 15:26:12 +01:00
Babib3l
fb881986ce Merge branch ryujinx:master into master 2025-11-12 15:32:58 +01:00
Babib3l
f5d87f3bb7 Merge branch ryujinx:master into master 2025-11-11 00:59:25 +01:00
Babib3l
8ddb0c16c3 Merge branch ryujinx:master into master 2025-11-10 23:46:28 +01:00
Babib3l
54f08acf2c Merge branch ryujinx:master into master 2025-11-10 15:37:20 +01:00
Babib3l
3c550deeb7 Merge branch ryujinx:master into master 2025-11-08 16:31:23 +01:00
Babib3l
f2e2e93ea2 Merge branch ryujinx:master into master 2025-11-07 18:38:10 +01:00
Babib3l
3361ad933f Merge branch ryujinx:master into master 2025-11-05 17:41:48 +01:00
Babib3l
27c3231433 nullification of a french translation 2025-10-31 18:00:21 +01:00
Babib3l
3d25b9940e Merge branch ryujinx:master into master 2025-10-31 17:27:32 +01:00
Babib3l
b5f6e68e55 Merge branch ryujinx:master into master 2025-10-30 11:01:10 +01:00
Babib3l
69ec2ef1be Merge branch ryujinx:master into master 2025-10-29 20:30:26 +01:00
Babib3l
07491eeaf4 Merge branch ryujinx:master into master 2025-10-28 13:57:59 +01:00
Babib3l
d9d9c69a15 Merge branch ryujinx:master into master 2025-10-27 13:31:12 +01:00
Babib3l
5327853f72 Update file locales.json 2025-10-26 22:10:25 +01:00
Babib3l
1ab78040aa more general fixes 2025-10-26 22:07:17 +01:00
Babib3l
726491d0ba Fix Debug being Deboguage in some fr_FR translations for consistency 2025-10-26 21:59:20 +01:00
Babib3l
b1bd469897 Update file locales.json 2025-10-26 21:53:23 +01:00
Babib3l
2c53c5bb06 Merge branch ryujinx:master into master 2025-10-26 21:52:09 +01:00
Babib3l
2a74d2284d Capitalisation fix 2025-10-26 11:12:44 +01:00
Babib3l
c980dc00aa Update file locales.json 2025-10-26 11:10:15 +01:00
Babib3l
c7c8086f9f Merge branch ryujinx:master into master 2025-10-26 11:01:11 +01:00
Babib3l
0c8c1be821 Merge branch ryujinx:master into master 2025-10-25 13:56:44 +02:00
Babib3l
1c3ed0d168 Merge branch ryujinx:master into master 2025-10-24 18:37:51 +02:00
Babib3l
a605f7fafc Merge branch ryujinx:master into master 2025-10-24 15:21:19 +02:00
Babib3l
03ee34e016 Merge branch ryujinx:master into master 2025-10-23 12:17:56 +02:00
Babib3l
8dff5a2556 Merge branch ryujinx:master into master 2025-10-22 20:06:58 +02:00
Babib3l
75faee906d Merge branch ryujinx:master into master 2025-10-21 14:47:14 +02:00
Babib3l
9beb4efb56 Merge branch ryujinx:master into master 2025-10-20 10:55:22 +02:00
Babib3l
914d4c8a79 Update file locales.json 2025-10-18 19:05:40 +02:00
Babib3l
2a999912ea Merge branch ryujinx:master into master 2025-10-18 18:41:46 +02:00
Babib3l
c02263abd7 Update file locales.json 2025-10-18 18:41:35 +02:00
Babib3l
02c7d0706a Merge branch ryujinx:master into master 2025-10-16 13:22:54 +02:00
Babib3l
028425982c Merge branch ryujinx:master into master 2025-10-14 19:15:24 +02:00
Babib3l
a2bb436e40 Merge branch ryujinx:master into master 2025-10-12 13:10:27 +02:00
Babib3l
9e1f6db406 Merge branch ryujinx:master into master 2025-09-30 17:03:59 +02:00
Babib3l
f84ee55307 Merge branch ryujinx:master into master 2025-09-05 13:07:45 +02:00
Babib3l
f045f4acd4 Merge branch ryujinx:master into master 2025-09-01 19:26:03 +02:00
Babib3l
f389415b0a Merge branch ryujinx:master into master 2025-08-31 15:36:29 +02:00
Babib3l
b4bde4ccb8 Merge branch ryujinx:master into master 2025-08-28 21:46:19 +02:00
Babib3l
c76eda2c1a Update file locales.json 2025-08-28 15:54:51 +02:00
Babib3l
59eba8f38b Update file locales.json 2025-08-28 15:48:41 +02:00
Babib3l
fc62ae41ae Update file locales.json 2025-08-28 14:52:22 +02:00
Babib3l
127d0c7ac1 Update file locales.json 2025-08-28 14:10:51 +02:00
Babib3l
15b44cfea6 Merge branch ryujinx:master into master 2025-08-28 14:08:37 +02:00
Babib3l
07eddefc95 Merge branch ryujinx:master into master 2025-08-27 13:03:36 +02:00
Babib3l
e3ea13bc45 Update file locales.json 2025-08-25 12:48:13 +02:00
Babib3l
0684d60c8c Merge branch ryujinx:master into master 2025-08-25 12:43:20 +02:00
Babib3l
6bf57c5ffb Merge branch ryujinx:master into master 2025-08-24 18:35:45 +02:00
Babib3l
e4a927f7a1 Merge branch ryujinx:master into master 2025-08-02 13:31:33 +02:00
Babib3l
e1e4c111d1 Merge branch ryujinx:master into master 2025-07-28 13:38:11 +02:00
Babib3l
0b790469a8 Merge branch ryujinx:master into master 2025-07-17 04:43:37 +02:00
Babib3l
385e9c869f Update file locales.json 2025-07-16 15:20:04 +02:00
Babib3l
c5528d59a0 Update file locales.json 2025-07-15 15:46:52 +02:00
Babib3l
6f113c4175 Update file locales.json 2025-07-15 15:43:12 +02:00
81 changed files with 4703 additions and 3320 deletions

View File

@@ -1,204 +0,0 @@
name: Build PR
on:
pull_request:
branches: [ master ]
paths:
- '**'
- '!.forgejo/**'
- '!*.yml'
- '!*.config'
- '!*.md'
- '.forgejo/workflows/*.yml'
env:
POWERSHELL_TELEMETRY_OPTOUT: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
RELEASE: 0
jobs:
build:
name: ${{ matrix.platform.name }} (${{ matrix.configuration }})
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
timeout-minutes: 45
strategy:
matrix:
configuration: [Release]
platform:
- { name: win-x64, zip_os_name: win_x64 }
#- { name: win-arm64, zip_os_name: win_arm64 }
- { name: linux-x64, zip_os_name: linux_x64 }
- { name: linux-arm64, zip_os_name: linux_arm64 }
#- { name: osx-x64, zip_os_name: osx_x64 }
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install 7zip
run: |
sudo apt update && sudo apt install -y 7zip
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.forgejo/csc.json"
- name: Get version info
id: version_info
run: |
echo "result=$(gli get-next-version -c Canary -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
shell: bash
- name: Change config filename
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
if: forgejo.event_name == 'pull_request'
- name: 'Cache: ~/.nuget/packages'
uses: actions/cache@v5
with:
path: |
~/.nuget/packages
key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
- name: Build
run: dotnet build -c "${{ matrix.configuration }}" -p:Version="${{ steps.version_info.outputs.result }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:ExtraDefineConstants=DISABLE_UPDATER
- name: Test
uses: actions/unstable-commands@v1
with:
commands: dotnet test --no-build -c "${{ matrix.configuration }}"
timeout-minutes: 10
retry-codes: 139
if: matrix.platform.name != 'linux-arm64'
- name: Publish Ryujinx
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.result }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained
if: forgejo.event_name == 'pull_request'
- name: Packing Windows builds
if: contains(matrix.platform.name, 'win')
run: |
7z a artifact/ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}.7z publish
shell: bash
- name: Upload Ryujinx Windows artifact
uses: actions/upload-artifact@v4
with:
name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}
path: artifact
if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'win')
- name: Build AppImage
if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux')
run: |
chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh
PLATFORM_NAME="${{ matrix.platform.name }}"
sudo apt update && sudo apt install -y zsync desktop-file-utils appstream libfuse2t64
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
# Setup appimagetool
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x tools/appimagetool
chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
ARCH_NAME=x64
export ARCH=x86_64
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
ARCH_NAME=arm64
export ARCH=aarch64
else
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
exit 1
fi
BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
shell: bash
- name: Upload Ryujinx AppImage artifact
uses: actions/upload-artifact@v4
if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux')
with:
name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}-AppImage
path: publish_appimage
build_macos:
name: macOS Universal (${{ matrix.configuration }})
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
matrix:
configuration: [ Release ]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install rcodesign
run: |
gli ghr -R indygreg/apple-platform-rs -p apple-codesign-*-x86_64-unknown-linux-musl.tar.gz -O apple-codesign.tar.gz
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
rm apple-codesign.tar.gz
sudo mv rcodesign /usr/bin/rcodesign
- name: Get version info
id: version_info
run: |
echo "result=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
shell: bash
- name: Change config filename
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
if: forgejo.event_name == 'pull_request'
- name: 'Cache: ~/.nuget/packages'
uses: actions/cache@v5
with:
path: |
~/.nuget/packages
key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
- name: Publish macOS Ryujinx
run: |
bash distribution/macos/create_macos_pr_build_ava.sh . publish_tmp publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.result }}" "${{ steps.version_info.outputs.git_short_hash }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
shell: bash
- name: Upload Ryujinx artifact
uses: actions/upload-artifact@v4
with:
name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal
path: "publish/*.tar.gz"
if: forgejo.event_name == 'pull_request'

86
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
name: Bug Report
description: File a bug report
title: "[Bug]"
labels: bug
body:
- type: textarea
id: issue
attributes:
label: Description of the issue
description: What's the issue you encountered?
validations:
required: true
- type: textarea
id: repro
attributes:
label: Reproduction steps
description: How can the issue be reproduced?
placeholder: Describe each step as precisely as possible
validations:
required: true
- type: textarea
id: log
attributes:
label: Log file
description: "A log file will help our developers to better diagnose and fix the issue. UPLOAD THE FILE. DO NOT COPY AND PASTE THE FILE'S CONTENT."
placeholder: Logs files can be found under "Logs" folder in Ryujinx program folder. They can also be accessed by opening Ryujinx, then going to File > Open Logs Folder. You can drag and drop the log on to the text area (do not copy paste).
validations:
required: true
- type: input
id: os
attributes:
label: OS
placeholder: "e.g. Windows 10"
validations:
required: true
- type: input
id: ryujinx-version
attributes:
label: Ryujinx version
placeholder: "e.g. 1.0.470"
validations:
required: true
- type: input
id: game-version
attributes:
label: Game version
placeholder: "e.g. 1.1.1"
validations:
required: false
- type: input
id: cpu
attributes:
label: CPU
placeholder: "e.g. i7-6700"
validations:
required: false
- type: input
id: gpu
attributes:
label: GPU
placeholder: "e.g. NVIDIA RTX 2070"
validations:
required: false
- type: input
id: ram
attributes:
label: RAM
placeholder: "e.g. 16GB"
validations:
required: false
- type: textarea
id: mods
attributes:
label: List of applied mods
placeholder: You can list applied mods here.
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context?
description: |
- Additional info about your environment:
- Any other information relevant to your issue.
validations:
required: false

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Ryujinx Discord
url: https://discord.gg/N2FmfVc
about: This is for development related issues. For support and technical issues, please come to our Discord server.

View File

@@ -0,0 +1,31 @@
name: Feature Request
description: Suggest a new feature for Ryujinx.
title: "[Feature Request]"
labels: enhancement
body:
- type: textarea
id: overview
attributes:
label: Overview
description: Include the basic, high-level concepts for this feature here.
validations:
required: true
- type: textarea
id: details
attributes:
label: Smaller details
description: These may include specific methods of implementation etc.
validations:
required: true
- type: textarea
id: request
attributes:
label: Nature of request
validations:
required: true
- type: textarea
id: feature
attributes:
label: Why would this feature be useful?
validations:
required: true

View File

@@ -0,0 +1,26 @@
name: Missing CPU Instruction
description: CPU Instruction is missing in Ryujinx.
title: "[CPU]"
labels: [cpu, not-implemented]
body:
- type: textarea
id: instruction
attributes:
label: CPU instruction
description: What CPU instruction is missing?
validations:
required: true
- type: textarea
id: name
attributes:
label: Instruction name
description: Include the name from [armconverter.com](https://armconverter.com/?disasm) or [shell-storm.org](http://shell-storm.org/online/Online-Assembler-and-Disassembler/?arch=arm64&endianness=big&dis_with_raw=True&dis_with_ins=True) in the above code block
validations:
required: true
- type: textarea
id: required
attributes:
label: Required by
description: Add links to the [compatibility list page(s)](https://github.com/Ryujinx/Ryujinx-Games-List/issues) of the game(s) that require this instruction.
validations:
required: true

View File

@@ -0,0 +1,25 @@
name: Missing Service Call
description: Service call is missing in Ryujinx.
labels: not-implemented
body:
- type: textarea
id: instruction
attributes:
label: Service call
description: What service call is missing?
validations:
required: true
- type: textarea
id: name
attributes:
label: Service description
description: Include the description/explanation from [Switchbrew](https://switchbrew.org/w/index.php?title=Services_API) and/or [SwIPC](https://reswitched.github.io/SwIPC/) in the above code block
validations:
required: true
- type: textarea
id: required
attributes:
label: Required by
description: Add links to the [compatibility list page(s)](https://github.com/Ryujinx/Ryujinx-Games-List/issues) of the game(s) that require this service.
validations:
required: true

View File

@@ -0,0 +1,19 @@
name: Missing Shader Instruction
description: Shader Instruction is missing in Ryujinx.
title: "[GPU]"
labels: [gpu, not-implemented]
body:
- type: textarea
id: instruction
attributes:
label: Shader instruction
description: What shader instruction is missing?
validations:
required: true
- type: textarea
id: required
attributes:
label: Required by
description: Add links to the [compatibility list page(s)](https://github.com/Ryujinx/Ryujinx-Games-List/issues) of the game(s) that require this instruction.
validations:
required: true

View File

@@ -10,10 +10,6 @@ gpu:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.Graphics.*/**', 'src/Spv.Generator/**', 'src/Ryujinx.ShaderTools/**']
input:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.Input*/**', 'src/Ryujinx/UI/Views/Input/**']
'graphics-backend:opengl':
- changed-files:
- any-glob-to-any-file: 'src/Ryujinx.Graphics.OpenGL/**'
@@ -22,17 +18,17 @@ input:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.Graphics.Vulkan/**', 'src/Spv.Generator/**']
'graphics-backend:metal':
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.Graphics.Metal/**', 'src/Ryujinx.Graphics.Metal.SharpMetalExtensions/**']
gui:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.LocaleGenerator/**']
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**']
'horizon/hle':
horizon:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.HLE/**', 'src/Ryujinx.HLE.Generators/**', 'src/Ryujinx.Horizon/**']
i18n:
- changed-files:
- any-glob-to-any-file: ['assets/**/*.json', 'src/Ryujinx.UI.LocaleGenerator/**']
- any-glob-to-any-file: ['src/Ryujinx.HLE/**', 'src/Ryujinx.Horizon/**']
kernel:
- changed-files:
@@ -40,7 +36,7 @@ kernel:
infra:
- changed-files:
- any-glob-to-any-file: ['.forgejo/**', 'distribution/**', 'Directory.Packages.props', 'src/Ryujinx.BuildValidationTasks/**']
- any-glob-to-any-file: ['.github/**', 'distribution/**', 'Directory.Packages.props', 'src/Ryujinx.BuildValidationTasks/**']
documentation:
- changed-files:
@@ -48,4 +44,4 @@ documentation:
ldn:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.HLE/HOS/Services/Ldn/**', 'src/Ryujinx/UI/Windows/LdnGamesListWindow.*', 'src/Ryujinx/UI/ViewModels/LdnGamesListViewModel.cs']
- any-glob-to-any-file: 'src/Ryujinx.HLE/HOS/Services/Ldn/**'

168
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,168 @@
name: Build job
on:
workflow_call:
env:
POWERSHELL_TELEMETRY_OPTOUT: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
RYUJINX_BASE_VERSION: "1.2.0"
RELEASE: 0
jobs:
build:
name: ${{ matrix.platform.name }} (${{ matrix.configuration }})
runs-on: ${{ matrix.platform.os }}
timeout-minutes: 45
strategy:
matrix:
configuration: [Debug, Release]
platform:
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
- { name: win-arm64, os: windows-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
- { name: osx-x64, os: macos-13, zip_os_name: osx_x64 }
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.github/csc.json"
- name: Get git short hash
id: git_short_hash
run: echo "result=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Change config filename
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Change config filename for macOS
run: sed -r -i '' 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
if: github.event_name == 'pull_request' && matrix.platform.os == 'macos-13'
- name: Build
run: dotnet build -c "${{ matrix.configuration }}" -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER
- name: Test
uses: TSRBerry/unstable-commands@v1
with:
commands: dotnet test --no-build -c "${{ matrix.configuration }}"
timeout-minutes: 10
retry-codes: 139
if: matrix.platform.name != 'linux-arm64'
- name: Publish Ryujinx
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Set executable bit
run: |
chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
- name: Build AppImage
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
run: |
PLATFORM_NAME="${{ matrix.platform.name }}"
sudo apt install -y zsync desktop-file-utils appstream
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
# Setup appimagetool
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x tools/appimagetool
chmod +x distribution/linux/appimage/build-appimage.sh
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
ARCH_NAME=x64
export ARCH=x86_64
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
ARCH_NAME=arm64
export ARCH=aarch64
else
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
exit 1
fi
export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
shell: bash
- name: Upload Ryujinx artifact
uses: actions/upload-artifact@v4
with:
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
path: publish
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
- name: Upload Ryujinx (AppImage) artifact
uses: actions/upload-artifact@v4
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
with:
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}-AppImage
path: publish_appimage
build_macos:
name: macOS Universal (${{ matrix.configuration }})
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
matrix:
configuration: [ Debug, Release ]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install rcodesign
run: |
mkdir -p $HOME/.bin
gh release download -R indygreg/apple-platform-rs -O apple-codesign.tar.gz -p 'apple-codesign-*-x86_64-unknown-linux-musl.tar.gz'
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
rm apple-codesign.tar.gz
mv rcodesign $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get git short hash
id: git_short_hash
run: echo "result=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
- name: Change config filename
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
shell: bash
if: github.event_name == 'pull_request'
- name: Publish macOS Ryujinx
run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp publish ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
- name: Upload Ryujinx artifact
uses: actions/upload-artifact@v4
with:
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
path: "publish/*.tar.gz"
if: github.event_name == 'pull_request'

View File

@@ -6,7 +6,7 @@ on:
push:
branches: [ master ]
paths-ignore:
- '.forgejo/**'
- '.github/**'
- 'docs/**'
- 'assets/**'
- '*.yml'
@@ -25,41 +25,44 @@ env:
jobs:
release:
name: Release for ${{ matrix.platform.name }}
runs-on: docker
container:
image: ${{ matrix.platform.os }}
runs-on: ${{ matrix.platform.os }}
strategy:
matrix:
platform:
- { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
- { name: win-x64, os: ubuntu-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: ubuntu-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v5
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.forgejo/csc.json"
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
run: echo "::add-matcher::.github/csc.json"
- name: Install 7zip
run: |
sudo apt update && sudo apt install -y 7zip
sudo apt install -y 7zip
- name: Install gli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.31
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
echo "build_version=$(gli get-next-version -c Canary -R)" >> $FORGEJO_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Configure for release
@@ -84,9 +87,12 @@ jobs:
pushd publish
rm libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.7z ../publish
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Packing Linux builds
if: contains(matrix.platform.name, 'linux')
@@ -95,8 +101,9 @@ jobs:
rm libarmeilleure-jitsupport.dylib
chmod +x Ryujinx.sh Ryujinx
tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
tar -cJvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.xz ../publish
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
shell: bash
- name: Build AppImage (Linux)
@@ -105,7 +112,7 @@ jobs:
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
PLATFORM_NAME="${{ matrix.platform.name }}"
sudo apt update && sudo apt install -y zsync desktop-file-utils appstream libfuse2t64
sudo apt install -y zsync desktop-file-utils appstream
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
@@ -132,28 +139,17 @@ jobs:
pushd publish_appimage
mv Ryujinx.AppImage ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
shell: bash
- name: Create release
uses: actions/create-release@v1
with:
name: "Canary ${{ steps.version_info.outputs.build_version }}"
body: "**Full Changelog:** [`${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}`](https://git.ryujinx.app/projects/Ryubing/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
repository: "Ryubing/Canary"
token: ${{ secrets.RELEASER_TOKEN }}
tag_name: ${{ steps.version_info.outputs.build_version }}
files: |-
release_output/**
macos_release:
name: Release MacOS universal
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v5
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
@@ -163,24 +159,33 @@ jobs:
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install gli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.30
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install rcodesign
run: |
gli ghr -R indygreg/apple-platform-rs -p apple-codesign-*-x86_64-unknown-linux-musl.tar.gz -O apple-codesign.tar.gz
mkdir -p $HOME/.bin
gh release download -R indygreg/apple-platform-rs -O apple-codesign.tar.gz -p 'apple-codesign-*-x86_64-unknown-linux-musl.tar.gz'
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
rm apple-codesign.tar.gz
mv rcodesign /usr/bin/rcodesign
mv rcodesign $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
echo "build_version=$(gli get-next-version -c Canary -R)" >> $FORGEJO_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Configure for release
@@ -196,53 +201,46 @@ jobs:
- name: Publish macOS Ryujinx
run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
- name: Create release
uses: actions/create-release@v1
with:
name: "Canary ${{ steps.version_info.outputs.build_version }}"
body: "**Full Changelog:** [`${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}`](https://git.ryujinx.app/projects/Ryubing/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
repository: "Ryubing/Canary"
token: ${{ secrets.RELEASER_TOKEN }}
tag_name: ${{ steps.version_info.outputs.build_version }}
files: |-
publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
post_ci:
name: Post CI Steps
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
create_gitlab_release:
name: Create GitLab Release
runs-on: ubuntu-24.04
needs:
- macos_release
- release
steps:
- uses: actions/checkout@v4
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install gli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.30
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
echo "build_version=$(gli get-next-version -c Canary -R)" >> $FORGEJO_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Create tag
run: |
gli create-tag -T ${{ secrets.RELEASER_TOKEN }} -P projects/Ryubing -n Canary-${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }}
- name: Link to actual source archives for Canary
run: |
gli canary-release -T ${{ secrets.RELEASER_TOKEN }} -P Ryubing/Canary -r ${{ steps.version_info.outputs.build_version }}
gli create-tag -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Canary-${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }}
- name: Create release
run: |
gli create-release-from-generic-package-files -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r main -t "Canary ${{ steps.version_info.outputs.build_version }}" -b "**Full Changelog:** [${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
- name: Send notification webhook
run: |
gli send-update-message -T ${{ secrets.RELEASER_TOKEN }} -P Ryubing/Canary -t ${{ steps.version_info.outputs.build_version }} -c FF4500 -w ${{ secrets.CANARY_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
gli send-update-message -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -t ${{ steps.version_info.outputs.build_version }} -c FF4500 -w ${{ secrets.CANARY_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
- name: Notify update server of new builds
run: |

25
.github/workflows/checks.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Build PR
on:
pull_request:
branches: [ master ]
paths:
- '**'
- '!.github/**'
- '!*.yml'
- '!*.config'
- '!*.md'
- '.github/workflows/*.yml'
permissions:
pull-requests: write
checks: write
concurrency:
group: pr-checks-${{ github.event.number }}
cancel-in-progress: true
jobs:
pr_build:
uses: ./.github/workflows/build.yml
secrets: inherit

View File

@@ -5,6 +5,10 @@ on:
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
@@ -14,13 +18,11 @@ jobs:
with:
# Ensure we pin the source origin as pull_request_target run under forks.
fetch-depth: 0
repository: projects/Ryubing
repository: GreemDev/Ryujinx
ref: master
- name: Update labels based on changes
uses: actions/labeler@v6
uses: actions/labeler@v5
with:
repo-token: ${{ secrets.LABELER_TOKEN }}
configuration-path: .forgejo/labeler.yml
sync-labels: true
dot: true

View File

@@ -19,20 +19,18 @@ env:
jobs:
release:
name: Release for ${{ matrix.platform.name }}
runs-on: docker
container:
image: ${{ matrix.platform.os }}
runs-on: ${{ matrix.platform.os }}
strategy:
matrix:
platform:
- { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
- { name: win-x64, os: ubuntu-latest, zip_os_name: win_x64 }
#- { name: win-arm64, os: ubuntu-latest, zip_os_name: win_arm64 }
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v5
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
@@ -43,21 +41,26 @@ jobs:
run: |
sudo apt install -y 7zip
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install gli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.31
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $FORGEJO_OUTPUT
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
else
echo "build_version=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
fi
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Configure for release
@@ -81,9 +84,12 @@ jobs:
pushd publish
rm libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.7z ../publish
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Packing Linux builds
if: contains(matrix.platform.name, 'linux')
@@ -92,9 +98,12 @@ jobs:
rm libarmeilleure-jitsupport.dylib
chmod +x Ryujinx.sh Ryujinx
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
tar -cJvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.xz ../publish
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build AppImage (Linux)
if: contains(matrix.platform.name, 'linux')
@@ -129,27 +138,17 @@ jobs:
pushd publish_appimage
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
popd
shell: bash
- name: Create release
uses: actions/create-release@v1
with:
name: "${{ steps.version_info.outputs.build_version }}"
repository: "projects/Ryubing"
token: ${{ secrets.RELEASER_TOKEN }}
tag_name: ${{ steps.version_info.outputs.build_version }}
files: |-
release_output/**
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
shell: bash
macos_release:
name: Release MacOS universal
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v5
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
@@ -159,28 +158,37 @@ jobs:
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install gli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.30
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install rcodesign
run: |
gli ghr -R indygreg/apple-platform-rs -p apple-codesign-*-x86_64-unknown-linux-musl.tar.gz -O apple-codesign.tar.gz
mkdir -p $HOME/.bin
gh release download -R indygreg/apple-platform-rs -O apple-codesign.tar.gz -p 'apple-codesign-*-x86_64-unknown-linux-musl.tar.gz'
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
rm apple-codesign.tar.gz
mv rcodesign /usr/bin/rcodesign
mv rcodesign $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $FORGEJO_OUTPUT
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
else
echo "build_version=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
fi
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Configure for release
@@ -193,20 +201,12 @@ jobs:
- name: Publish macOS Ryujinx
run: |
bash distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
- name: Create release
uses: actions/create-release@v1
with:
name: "${{ steps.version_info.outputs.build_version }}"
repository: "projects/Ryubing"
token: ${{ secrets.RELEASER_TOKEN }}
tag_name: ${{ steps.version_info.outputs.build_version }}
files: |-
publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
post_ci:
name: Post-CI Steps
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
create_gitlab_release:
name: Create GitLab Release
runs-on: ubuntu-24.04
needs:
- macos_release
@@ -214,26 +214,36 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install gli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64' 2.0.30
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get version info
id: version_info
run: |
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $FORGEJO_OUTPUT
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
else
echo "build_version=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
fi
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
echo "commit_message=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
shell: bash
- name: Create release
run: |
gli create-release-from-generic-package-files -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }} -t "${{ steps.version_info.outputs.build_version }}" -b "msd:${{ steps.version_info.outputs.build_version }}"
- name: Send notification webhook
run: |
gli send-update-message -T ${{ secrets.RELEASER_TOKEN }} -P projects/Ryubing -t ${{ steps.version_info.outputs.build_version }} -c 32cd32 -w ${{ secrets.STABLE_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
gli send-update-message -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -t ${{ steps.version_info.outputs.build_version }} -c 32cd32 -w ${{ secrets.STABLE_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
- name: Notify update server of new builds
run: |

3
.gitignore vendored
View File

@@ -72,6 +72,9 @@ ipch/
_ReSharper*/
*.[Rr]e[Ss]harper
#.NET
.dotnet-home/
# TeamCity is a build add-in
_TeamCity*

View File

@@ -8,7 +8,6 @@
<PackageVersion Include="Avalonia.Desktop" Version="11.3.12" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.12" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.12" />
<PackageVersion Include="SharpCompress" Version="0.47.4" />
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.9.4" />
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.9.4" />
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
@@ -17,7 +16,7 @@
<PackageVersion Include="Projektanker.Icons.Avalonia" Version="9.6.2" />
<PackageVersion Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" />
<PackageVersion Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.6.2" />
<PackageVersion Include="Ryujinx.SDL3-CS" Version="2026.426.0" />
<PackageVersion Include="ppy.SDL3-CS" Version="2025.920.0" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="Concentus" Version="2.2.2" />
@@ -43,12 +42,13 @@
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.2-build3" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.129" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="2.0.6" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="2.0.6" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
<PackageVersion Include="Gommon" Version="2.8.0.1" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.11.1" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.22.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.22.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.22.0" />

View File

@@ -7,8 +7,8 @@
# Ryujinx
[![Latest release](https://git.ryujinx.app/projects/Ryubing/badges/release.svg?label=stable&color=32cd32)](https://update.ryujinx.app/latest/stable)
[![Latest canary release](https://git.ryujinx.app/Ryubing/Canary/badges/release.svg?label=canary&color=FF4500)](https://update.ryujinx.app/latest/canary)
[![Latest release](https://img.shields.io/gitlab/v/release/ryubing%2Fryujinx?gitlab_url=https%3A%2F%2Fgit.ryujinx.app&label=stable&color=32cd32)](https://update.ryujinx.app/latest/stable)
[![Latest canary release](https://img.shields.io/gitlab/v/release/ryubing%2Fcanary?gitlab_url=https%3A%2F%2Fgit.ryujinx.app&label=canary&color=FF4500)](https://update.ryujinx.app/latest/canary)
<br>
<a href="https://discord.gg/PEuzjrFXUA">
<img src="https://img.shields.io/discord/1294443224030511104?color=5865F2&label=Ryubing&logo=discord&logoColor=white" alt="Discord">
@@ -21,7 +21,7 @@
Ryujinx is an open-source Nintendo Switch emulator, originally created by gdkchan, written in C#.
This emulator aims at providing excellent accuracy and performance, a user-friendly interface and consistent builds.
It was written from scratch and development on the project began in September 2017.
Ryujinx is available on a self-managed <a href="https://github.com/Ryubing/forgejo" target="_blank">modified</a> <a href="https://forgejo.org/" target="_blank">Forgejo</a> instance under the <a href="https://git.ryujinx.app/projects/Ryubing/src/branch/master/LICENSE.txt" target="_blank">MIT license</a>.
Ryujinx is available on a self-managed GitLab instance under the <a href="https://git.ryujinx.app/ryubing/ryujinx/-/blob/master/LICENSE.txt?ref_type=heads" target="_blank">MIT license</a>.
<br />
</p>
<p align="center">
@@ -31,11 +31,11 @@
<br>
This is not a Ryujinx revival project. This is not a Phoenix project.
<br>
Guides and documentation can be found on the <a href="https://git.ryujinx.app/projects/Ryubing/wiki/Home">Wiki tab</a>.
Guides and documentation can be found on the <a href="https://git.ryujinx.app/groups/ryubing/-/wikis/home">Wiki tab</a>.
</p>
<p align="center">
<img src="https://git.ryujinx.app/projects/Ryubing/raw/branch/master/docs/shell.png" alt="Ryujinx example">
<img src="https://git.ryujinx.app/ryubing/ryujinx/-/raw/master/docs/shell.png?ref_type=heads&inline=false" alt="Ryujinx example">
</p>
## Usage
@@ -49,17 +49,17 @@ Stable builds are made every so often, based on the `master` branch, that then g
These stable builds exist so that the end user can get a more **enjoyable and stable experience**.
They are released every month or so, to ensure consistent updates, while not being an annoying amount of individual updates to download over the course of that month.
You can find the stable releases [here](https://git.ryujinx.app/projects/Ryubing/releases).
You can find the stable releases [here](https://git.ryujinx.app/ryubing/ryujinx/-/releases).
Canary builds are compiled automatically for each commit on the `master` branch.
While we strive to ensure optimal stability and performance prior to pushing an update, these builds **may be unstable or completely broken**.
These canary builds are only recommended for experienced users.
You can find the canary releases [here](https://git.ryujinx.app/Ryubing/Canary/releases).
You can find the canary releases [here](https://git.ryujinx.app/ryubing/canary/-/releases).
## Documentation
If you are planning to contribute or just want to learn more about this project please read through our [documentation](https://git.ryujinx.app/projects/Ryubing/src/branch/master/docs/README.md).
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
## Features
@@ -105,13 +105,13 @@ If you are planning to contribute or just want to learn more about this project
## License
This software is licensed under the terms of the [MIT license](https://git.ryujinx.app/projects/Ryubing/src/branch/master/LICENSE.txt).
This software is licensed under the terms of the [MIT license](LICENSE.txt).
This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3.
See [LICENSE.txt](https://git.ryujinx.app/projects/Ryubing/src/branch/master/LICENSE.txt) and [THIRDPARTY.md](https://git.ryujinx.app/projects/Ryubing/src/branch/master/distribution/legal/THIRDPARTY.md) for more details.
See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY.md) for more details.
## Credits
- [LibHac](https://git.ryujinx.app/projects/LibHac) is used for our file-system.
- [LibHac](https://git.ryujinx.app/ryubing/libhac) is used for our file-system.
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.

View File

@@ -86,11 +86,11 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.forgejo\workflows\build.yml = .forgejo\workflows\build.yml
.forgejo\workflows\canary.yml = .forgejo\workflows\canary.yml
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\canary.yml = .github\workflows\canary.yml
Directory.Packages.props = Directory.Packages.props
Directory.Build.props = Directory.Build.props
.forgejo\workflows\release.yml = .forgejo\workflows\release.yml
.github\workflows\release.yml = .github\workflows\release.yml
nuget.config = nuget.config
EndProjectSection
EndProject
@@ -573,16 +573,6 @@ Global
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x64.ActiveCfg = Debug|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x64.Build.0 = Debug|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x86.ActiveCfg = Debug|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x86.Build.0 = Debug|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|Any CPU.Build.0 = Release|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x64.ActiveCfg = Release|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x64.Build.0 = Release|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x86.ActiveCfg = Release|Any CPU
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "启动 RenderDoc 帧捕获",
"zh_TW": "啟動 RenderDoc 畫格擷取"
"zh_TW": ""
}
},
{
@@ -47,7 +47,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "结束 RenderDoc 帧捕获",
"zh_TW": "停止 RenderDoc 畫格擷取"
"zh_TW": ""
}
},
{
@@ -72,7 +72,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "丢弃 RenderDoc 帧捕获",
"zh_TW": "捨棄 RenderDoc 畫格擷取"
"zh_TW": ""
}
},
{
@@ -97,7 +97,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。",
"zh_TW": "停止正在執行的 RenderDoc 畫格擷取,且立即捨棄其結果。"
"zh_TW": ""
}
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -22,5 +22,5 @@ chmod +x AppDir/AppRun AppDir/usr/bin/Ryujinx*
mkdir -p "$OUTDIR"
appimagetool --appimage-extract-and-run -n --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 21 \
AppDir "$OUTDIR"/Ryujinx.AppImage
appimagetool -n --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 21 \
AppDir "$OUTDIR"/Ryujinx.AppImage

View File

@@ -1,120 +0,0 @@
#!/bin/bash
set -e
if [ "$#" -lt 8 ]; then
echo "usage <BASE_DIR> <TEMP_DIRECTORY> <OUTPUT_DIRECTORY> <ENTITLEMENTS_FILE_PATH> <VERSION> <SOURCE_REVISION_ID> <CONFIGURATION>"
exit 1
fi
mkdir -p "$1"
mkdir -p "$2"
mkdir -p "$3"
BASE_DIR=$(readlink -f "$1")
TEMP_DIRECTORY=$(readlink -f "$2")
OUTPUT_DIRECTORY=$(readlink -f "$3")
ENTITLEMENTS_FILE_PATH=$(readlink -f "$4")
VERSION=$5
SOURCE_REVISION_ID=$6
CONFIGURATION=$7
if [[ "$(uname)" == "Darwin" ]]; then
echo "Clearing xattr on all dot undercsore files"
find "$BASE_DIR" -type f -name "._*" -exec sh -c '
for f; do
dir=$(dirname "$f")
base=$(basename "$f")
orig="$dir/${base#._}"
[ -f "$orig" ] && xattr -c "$orig" || true
done
' sh {} +
fi
RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
ARM64_APP_BUNDLE="$TEMP_DIRECTORY/output_arm64/Ryujinx.app"
X64_APP_BUNDLE="$TEMP_DIRECTORY/output_x64/Ryujinx.app"
UNIVERSAL_APP_BUNDLE="$OUTPUT_DIRECTORY/Ryujinx.app"
EXECUTABLE_SUB_PATH=Contents/MacOS/Ryujinx
rm -rf "$TEMP_DIRECTORY"
mkdir -p "$TEMP_DIRECTORY"
DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" --self-contained true $EXTRA_ARGS)
dotnet restore
dotnet build -c "$CONFIGURATION" src/Ryujinx
dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
# Get rid of the support library for ARMeilleure for x64 (that's only for arm64)
rm -rf "$TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib"
# Get rid of libsoundio from arm64 builds as we don't have a arm64 variant
# TODO: remove this once done
rm -rf "$TEMP_DIRECTORY/publish_arm64/libsoundio.dylib"
pushd "$BASE_DIR/distribution/macos"
./create_app_bundle.sh "$TEMP_DIRECTORY/publish_x64" "$TEMP_DIRECTORY/output_x64" "$ENTITLEMENTS_FILE_PATH"
./create_app_bundle.sh "$TEMP_DIRECTORY/publish_arm64" "$TEMP_DIRECTORY/output_arm64" "$ENTITLEMENTS_FILE_PATH"
popd
rm -rf "$UNIVERSAL_APP_BUNDLE"
mkdir -p "$OUTPUT_DIRECTORY"
# Let's copy one of the two different app bundle and remove the executable
cp -R "$ARM64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE"
rm "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH"
# Make its libraries universal
python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_BUNDLE" "$X64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE" "**/*.dylib"
if ! [ -x "$(command -v lipo)" ];
then
if ! [ -x "$(command -v llvm-lipo-17)" ];
then
LIPO=llvm-lipo
else
LIPO=llvm-lipo-17
fi
else
LIPO=lipo
fi
# Make the executable universal
$LIPO "$ARM64_APP_BUNDLE/$EXECUTABLE_SUB_PATH" "$X64_APP_BUNDLE/$EXECUTABLE_SUB_PATH" -output "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH" -create
# Patch up the Info.plist to have appropriate version
sed -r -i.bck "s/\%\%RYUJINX_BUILD_VERSION\%\%/$VERSION/g;" "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist"
sed -r -i.bck "s/\%\%RYUJINX_BUILD_GIT_HASH\%\%/$SOURCE_REVISION_ID/g;" "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist"
rm "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist.bck"
# Now sign it
if ! [ -x "$(command -v codesign)" ];
then
if ! [ -x "$(command -v rcodesign)" ];
then
echo "Cannot find rcodesign on your system, please install rcodesign."
exit 1
fi
# NOTE: Currently require https://github.com/indygreg/apple-platform-rs/pull/44 to work on other OSes.
# cargo install --git "https://github.com/marysaka/apple-platform-rs" --branch "fix/adhoc-app-bundle" apple-codesign --bin "rcodesign"
echo "Using rcodesign for ad-hoc signing"
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$UNIVERSAL_APP_BUNDLE"
else
echo "Using codesign for ad-hoc signing"
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$UNIVERSAL_APP_BUNDLE"
fi
echo "Creating archive"
pushd "$OUTPUT_DIRECTORY"
tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf "$RELEASE_TAR_FILE_NAME" Ryujinx.app 1> /dev/null
python3 "$BASE_DIR/distribution/misc/add_tar_exec.py" "$RELEASE_TAR_FILE_NAME" "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
rm "$RELEASE_TAR_FILE_NAME"
popd
echo "Done"

View File

@@ -5,7 +5,8 @@
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<!-- Only needed when using pre-release versions of Ryujinx.LibHac. -->
<add key="LibHacAlpha" value="https://git.ryujinx.app/api/packages/projects/nuget/index.json" />
<add key="LibHacAlpha" value="https://git.ryujinx.app/api/v4/projects/17/packages/nuget/index.json" />
<add key="Ryujinx.UpdateClient" value="https://git.ryujinx.app/api/v4/projects/71/packages/nuget/index.json" />
</packageSources>
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
@@ -13,6 +14,10 @@
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="Ryujinx.UpdateClient">
<package pattern="Ryujinx.UpdateClient" />
<package pattern="Ryujinx.Systems.Update.Common" />
</packageSource>
<packageSource key="LibHacAlpha">
<package pattern="Ryujinx.LibHac" />
</packageSource>

View File

@@ -1,4 +1,4 @@
namespace Ryujinx.Common.Configuration.Hid.Keyboard
{
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<Key> { }
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<PhysicalKey> { }
}

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

View File

@@ -16,6 +16,15 @@ namespace Ryujinx.Common.Helper
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
[SupportedOSPlatform("windows")]
[LibraryImport("user32")]
private static partial nint GetForegroundWindow();
[SupportedOSPlatform("windows")]
[LibraryImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool SetForegroundWindow(nint hWnd);
public static bool SetConsoleWindowStateSupported => OperatingSystem.IsWindows();
public static void SetConsoleWindowState(bool show)
@@ -44,6 +53,10 @@ namespace Ryujinx.Common.Helper
return;
}
SetForegroundWindow(hWnd);
hWnd = GetForegroundWindow();
ShowWindow(hWnd, show ? SW_SHOW : SW_HIDE);
}
}

View File

@@ -29,8 +29,8 @@ namespace Ryujinx.Common
public static string GetChangelogUrl(Version currentVersion, Version newVersion) =>
IsCanaryBuild
? $"https://git.ryujinx.app/projects/Ryubing/compare/Canary-{currentVersion}...Canary-{newVersion}"
: $"https://git.ryujinx.app/projects/Ryubing/releases/tag/{newVersion}";
? $"https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-{currentVersion}...Canary-{newVersion}"
: $"https://git.ryujinx.app/ryubing/ryujinx/-/releases/{newVersion}";
}

View File

@@ -8,12 +8,12 @@ namespace Ryujinx.Common
public const string AmiiboTagsUrl = "https://raw.githubusercontent.com/Ryubing/Nfc/refs/heads/main/tags.json";
public const string FaqWikiUrl = "https://git.ryujinx.app/projects/Ryubing/wiki/FAQ-%26-Troubleshooting";
public const string FaqWikiUrl = "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/FAQ-&-Troubleshooting";
public const string SetupGuideWikiUrl =
"https://git.ryujinx.app/projects/Ryubing/wiki/Setup-%26-Configuration-Guide";
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
public const string MultiplayerWikiUrl =
"https://git.ryujinx.app/projects/Ryubing/wiki/Multiplayer-(LDN-Local-Wireless)-Guide";
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
}
}

View File

@@ -118,11 +118,8 @@ namespace Ryujinx.HLE.HOS.Services.Caps
}
// NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data.
using SKBitmap bitmap = new(new SKImageInfo(1280, 720, SKColorType.Rgba8888, SKAlphaType.Premul));
int dataLen = screenshotData.Length > bitmap.ByteCount ? bitmap.ByteCount : screenshotData.Length;
Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), dataLen);
using SKBitmap bitmap = new(new SKImageInfo(1280, 720, SKColorType.Rgba8888));
Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), screenshotData.Length);
using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
using FileStream file = File.OpenWrite(filePath);
data.SaveTo(file);

View File

@@ -151,9 +151,7 @@ namespace Ryujinx.Input.SDL3
result |= GamepadFeaturesFlag.Led;
}
SDL_UnlockProperties(propID);
// NOTE: Do not call SDL_DestroyProperties here. These properties are owned
// internally by SDL and are freed when SDL_CloseGamepad is called (in Dispose).
SDL_DestroyProperties(propID);
return result;
}

View File

@@ -331,18 +331,28 @@ namespace Ryujinx.Input.SDL3
public IEnumerable<IGamepad> GetGamepads()
{
string[] ids;
lock (_lock)
lock (_gamepadsIds)
{
ids = _gamepadsIds.Values
.Concat(_joyConsIds.Values)
.Concat(_linkedJoyConsIds.Values)
.ToArray();
foreach (var gamepad in _gamepadsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
foreach (string id in ids)
lock (_joyConsIds)
{
yield return GetGamepad(id);
foreach (var gamepad in _joyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
lock (_linkedJoyConsIds)
{
foreach (var gamepad in _linkedJoyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
}
}

View File

@@ -8,25 +8,15 @@ using System.Runtime.CompilerServices;
using System.Threading;
using SDL;
using static SDL.SDL3;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input.SDL3
{
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();
#pragma warning disable IDE0052 // Remove unread private member
private readonly SDL3KeyboardDriver _driver;
#pragma warning restore IDE0052
private StandardKeyboardInputConfig _configuration;
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private static readonly SDL_Keycode[] _keysDriverMapping =
@@ -171,9 +161,8 @@ namespace Ryujinx.Input.SDL3
SDL_Keycode.SDLK_0
];
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
public SDL3Keyboard(string id, string name)
{
_driver = driver;
Id = id;
Name = name;
_buttonsUserMapping = [];
@@ -195,9 +184,9 @@ namespace Ryujinx.Input.SDL3
}
[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;
}
@@ -205,18 +194,18 @@ namespace Ryujinx.Input.SDL3
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
{
Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
Key.AltRight => SDL_Keymod.SDL_KMOD_RALT,
Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
ConfigPhysicalKey.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
ConfigPhysicalKey.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
ConfigPhysicalKey.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
ConfigPhysicalKey.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
ConfigPhysicalKey.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
ConfigPhysicalKey.AltRight => SDL_Keymod.SDL_KMOD_RALT,
ConfigPhysicalKey.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
ConfigPhysicalKey.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
// NOTE: Menu key isn't supported by SDL3.
_ => SDL_Keymod.SDL_KMOD_NONE
};
@@ -232,9 +221,9 @@ namespace Ryujinx.Input.SDL3
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);
if (index == -1)
@@ -264,36 +253,6 @@ namespace Ryujinx.Input.SDL3
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()
{
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
@@ -306,9 +265,9 @@ namespace Ryujinx.Input.SDL3
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;
}
@@ -320,8 +279,8 @@ namespace Ryujinx.Input.SDL3
}
}
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -357,38 +316,15 @@ namespace Ryujinx.Input.SDL3
{
_configuration = (StandardKeyboardInputConfig)configuration;
// First clear the buttons mapping
_buttonsUserMapping.Clear();
// Then configure 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));
// 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));
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
}
}
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)

View File

@@ -50,7 +50,7 @@ namespace Ryujinx.Input.SDL3
return null;
}
return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
return new SDL3Keyboard(_keyboardIdentifers[0], "All keyboards");
}
public IEnumerable<IGamepad> GetGamepads()

View File

@@ -1,3 +1,5 @@
using Ryujinx.Common.Logging;
namespace Ryujinx.Input.Assigner
{
/// <summary>
@@ -8,22 +10,42 @@ namespace Ryujinx.Input.Assigner
private readonly IKeyboard _keyboard;
private KeyboardStateSnapshot _keyboardState;
private Button? _pressedButton;
public KeyboardKeyAssigner(IKeyboard keyboard)
{
_keyboard = keyboard;
}
public void Initialize() { }
public void Initialize()
{
_pressedButton = null;
}
public void ReadInput()
{
_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()
{
return GetPressedButton() is not null;
return _pressedButton is not null;
}
public bool ShouldCancel()
@@ -33,18 +55,53 @@ namespace Ryujinx.Input.Assigner
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++)
{
if (_keyboardState.IsPressed(key))
{
keyPressed = new(key);
break;
return new Button(key);
}
}
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;
}
}
}

View File

@@ -2,6 +2,7 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
@@ -234,7 +235,9 @@ namespace Ryujinx.Input.HLE
_gamepad?.Dispose();
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);
@@ -563,7 +566,7 @@ namespace Ryujinx.Input.HLE
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
_gamepad?.Rumble(low, high, uint.MaxValue);
_gamepad.Rumble(low, high, uint.MaxValue);
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
$"L.low.amp={leftVibrationValue.AmplitudeLow}, " +

View File

@@ -36,6 +36,7 @@ namespace Ryujinx.Input.HLE
private bool _isDisposed;
private List<InputConfig> _inputConfig;
private List<InputConfig> _requestedInputConfig;
private bool _enableKeyboard;
private bool _enableMouse;
private Switch _device;
@@ -52,6 +53,7 @@ namespace Ryujinx.Input.HLE
_gamepadDriver = gamepadDriver;
_mouseDriver = mouseDriver;
_inputConfig = [];
_requestedInputConfig = [];
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -89,14 +91,14 @@ namespace Ryujinx.Input.HLE
}
}
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
}
}
private void HandleOnGamepadConnected(string id)
{
// Force input reload
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -127,11 +129,13 @@ namespace Ryujinx.Input.HLE
{
lock (_lock)
{
_requestedInputConfig = inputConfig?.ToList() ?? [];
NpadController[] oldControllers = _controllers.ToArray();
List<InputConfig> validInputs = [];
foreach (InputConfig inputConfigEntry in inputConfig)
foreach (InputConfig inputConfigEntry in _requestedInputConfig)
{
NpadController controller;
int index = (int)inputConfigEntry.PlayerIndex;
@@ -147,7 +151,17 @@ namespace Ryujinx.Input.HLE
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)
{
@@ -157,7 +171,7 @@ namespace Ryujinx.Input.HLE
else
{
_controllers[index] = controller;
validInputs.Add(inputConfigEntry);
validInputs.Add(activeConfig);
}
}
@@ -169,7 +183,7 @@ namespace Ryujinx.Input.HLE
oldControllers[i] = null;
}
_inputConfig = inputConfig;
_inputConfig = validInputs;
_enableKeyboard = enableKeyboard;
_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()
{
lock (_lock)
@@ -334,7 +421,7 @@ namespace Ryujinx.Input.HLE
}
}
internal InputConfig GetPlayerInputConfigByIndex(int index)
public InputConfig GetPlayerInputConfigByIndex(int index)
{
lock (_lock)
{

View File

@@ -1,5 +1,6 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input
{
@@ -33,15 +34,26 @@ namespace Ryujinx.Input
{
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);
}
/// <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;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Ryujinx.Input
{
public interface IKeyboardModeDriver : IGamepadDriver
{
IKeyboard GetKeyboard(string id, KeyboardInputMode mode);
}
}

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

View File

@@ -0,0 +1,8 @@
namespace Ryujinx.Input
{
public enum KeyboardInputMode
{
Semantic,
Physical,
}
}

View File

@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input
{
@@ -25,5 +26,8 @@ namespace Ryujinx.Input
/// <returns>True if the given key is pressed</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPressed(Key key) => KeysState[(int)key];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPressed(ConfigPhysicalKey key) => KeysState[(int)key];
}
}

View File

@@ -10,7 +10,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ryujinx.SDL3-CS" />
<PackageReference Include="ppy.SDL3-CS" />
</ItemGroup>
</Project>

View File

@@ -61,14 +61,6 @@ namespace Ryujinx.SDL3.Common
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1");
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
// When hid_nintendo is loaded, it creates separate evdev devices for the gamepad
// and IMU which SDL3's evdev backend combines via UNIQ matching. Using HIDAPI
// instead conflicts with the kernel driver and breaks motion and hotplug.
if (OperatingSystem.IsLinux() && Directory.Exists("/sys/module/hid_nintendo"))
{
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "0");
}
// NOTE: As of SDL3 2.24.0, joycons are combined by default but the motion source only come from one of them.
// We disable this behavior for now.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "0");

View File

@@ -10,7 +10,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Parameter.Effect
public void EnsureTypeSize()
{
Assert.AreEqual(0x18, Unsafe.SizeOf<BiquadFilterEffectParameter1>());
Assert.AreEqual(0x28, Unsafe.SizeOf<BiquadFilterEffectParameter2>());
Assert.AreEqual(0x24, Unsafe.SizeOf<BiquadFilterEffectParameter2>());
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -24,7 +24,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
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
{
@@ -105,48 +105,48 @@ namespace Ryujinx.Headless
Backend = InputBackendType.WindowKeyboard,
Id = null,
ControllerType = ControllerType.JoyconPair,
LeftJoycon = new LeftJoyconCommonConfig<Key>
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{
DpadUp = Key.Up,
DpadDown = Key.Down,
DpadLeft = Key.Left,
DpadRight = Key.Right,
ButtonMinus = Key.Minus,
ButtonL = Key.E,
ButtonZl = Key.Q,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
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<Key>
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{
ButtonA = Key.Z,
ButtonB = Key.X,
ButtonX = Key.C,
ButtonY = Key.V,
ButtonPlus = Key.Plus,
ButtonR = Key.U,
ButtonZr = Key.O,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
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<Key>
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
},
};
}

View File

@@ -6,15 +6,15 @@ using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.Input
{
internal class AvaloniaKeyboard : IKeyboard
{
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private readonly AvaloniaKeyboardDriver _driver;
private readonly KeyboardInputMode _mode;
private StandardKeyboardInputConfig _configuration;
private readonly Lock _userMappingLock = new();
@@ -24,18 +24,12 @@ namespace Ryujinx.Ava.Input
public bool IsConnected => true;
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
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)
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name, KeyboardInputMode mode)
{
_buttonsUserMapping = [];
_driver = driver;
_mode = mode;
Id = id;
Name = name;
}
@@ -57,22 +51,18 @@ namespace Ryujinx.Ava.Input
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;
}
// NOTE: Do not touch state of the button already pressed.
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -100,7 +90,7 @@ namespace Ryujinx.Ava.Input
{
try
{
return _driver.IsPressed(key);
return _driver.IsPressed(key, _mode);
}
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)
{
lock (_userMappingLock)
@@ -116,37 +119,13 @@ namespace Ryujinx.Ava.Input
_buttonsUserMapping.Clear();
#pragma warning disable IDE0055 // Disable formatting
// 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
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
}
}
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) { }
@@ -162,41 +141,9 @@ namespace Ryujinx.Ava.Input
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()
{
_driver?.Clear();
_driver?.Clear(_mode);
}
public void Dispose() { }

View File

@@ -1,19 +1,37 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
using AvaKey = Avalonia.Input.Key;
using System.Threading;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using Key = Ryujinx.Input.Key;
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 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> KeyRelease;
@@ -22,14 +40,30 @@ namespace Ryujinx.Ava.Input
public string DriverName => "AvaloniaKeyboardDriver";
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
public AvaloniaKeyboardDriver(Control control)
public AvaloniaKeyboardDriver(Control control, KeyboardInputMode defaultMode = KeyboardInputMode.Semantic)
{
_control = control;
_pressedKeys = [];
_window = control as Window ?? TopLevel.GetTopLevel(control) as Window;
_semanticPressedKeys = [];
_physicalPressedKeys = [];
_observedPhysicalKeysBySemanticKey = [];
_semanticPressedKeyQueue = [];
_physicalPressedKeyQueue = [];
_pressedKeyQueueLock = new();
_defaultMode = defaultMode;
_control.KeyDown += OnKeyPress;
_control.KeyUp += OnKeyRelease;
// Use routed handlers so keys consumed earlier in the Avalonia pipeline
// 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;
_window?.Deactivated += Window_Deactivated;
}
private void Window_Deactivated(object sender, EventArgs e)
{
Clear();
}
private void Control_TextInput(object sender, TextInputEventArgs e)
@@ -50,13 +84,18 @@ namespace Ryujinx.Ava.Input
}
public IGamepad GetGamepad(string id)
{
return GetKeyboard(id, _defaultMode);
}
public IKeyboard GetKeyboard(string id, KeyboardInputMode mode)
{
if (!_keyboardIdentifers[0].Equals(id))
{
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")];
@@ -65,40 +104,189 @@ namespace Ryujinx.Ava.Input
{
if (disposing)
{
_control.KeyUp -= OnKeyPress;
_control.KeyDown -= OnKeyRelease;
_control.RemoveHandler(InputElement.KeyDownEvent, OnKeyPress);
_control.RemoveHandler(InputElement.KeyUpEvent, OnKeyRelease);
_control.TextInput -= Control_TextInput;
if (_window != null)
{
_window.Deactivated -= Window_Deactivated;
}
}
}
protected void OnKeyPress(object sender, KeyEventArgs args)
{
_pressedKeys.Add(args.Key);
UpdateKeyStates(args, isPressed: true);
KeyPressed?.Invoke(this, args);
}
protected void OnKeyRelease(object sender, KeyEventArgs args)
{
_pressedKeys.Remove(args.Key);
UpdateKeyStates(args, isPressed: false);
KeyRelease?.Invoke(this, args);
}
internal bool IsPressed(Key key)
internal bool IsPressed(Key key, KeyboardInputMode mode)
{
if (key is Key.Unbound or Key.Unknown)
{
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()
{
_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()

View File

@@ -2,6 +2,7 @@ using Ryujinx.Input;
using System;
using System.Collections.Generic;
using AvaKey = Avalonia.Input.Key;
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
namespace Ryujinx.Ava.Input
{
@@ -132,7 +133,8 @@ namespace Ryujinx.Ava.Input
AvaKey.D8,
AvaKey.D9,
AvaKey.OemTilde,
AvaKey.OemTilde,AvaKey.OemMinus,
AvaKey.Oem102,
AvaKey.OemMinus,
AvaKey.OemPlus,
AvaKey.OemOpenBrackets,
AvaKey.OemCloseBrackets,
@@ -147,7 +149,149 @@ namespace Ryujinx.Ava.Input
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<AvaPhysicalKey, Key> _avaPhysicalKeyMapping;
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.
_avaKeyMapping = new Dictionary<AvaKey, Key>();
_avaPhysicalKeyMapping = new Dictionary<AvaPhysicalKey, Key>();
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)
{
avaKey = AvaKey.None;
bool keyExist = (int)key < _keyMapping.Length;
bool keyExist = key < Key.Count && (int)key < _keyMapping.Length;
if (keyExist)
{
avaKey = _keyMapping[(int)key];
@@ -178,9 +343,34 @@ namespace Ryujinx.Ava.Input
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)
{
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);
}
}
}

View File

@@ -24,11 +24,9 @@ using Ryujinx.Headless;
using Ryujinx.SDL3.Common;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
namespace Ryujinx.Ava
@@ -54,22 +52,6 @@ namespace Ryujinx.Ava
if (OperatingSystem.IsWindows())
{
#if !DEBUG
// this fixes the "hide console" option by forcing the emulator to launch in an old-school cmd
if (!Console.Title.Contains("conhost.exe"))
{
StringBuilder sb = new();
foreach (string arg in args)
{
sb.Append(arg.Contains(' ') ? $" \"{arg}\"" : $" {arg}");
}
Process.Start("conhost.exe", $"{Environment.ProcessPath} {sb}");
return 0;
}
#endif
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
{
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning);

View File

@@ -46,7 +46,6 @@
<PackageReference Include="Avalonia.Diagnostics" Condition="'$(Configuration)'=='Debug'" />
<PackageReference Include="Avalonia.Controls.DataGrid" />
<PackageReference Include="Avalonia.Markup.Xaml.Loader" />
<PackageReference Include="SharpCompress" />
<PackageReference Include="Svg.Controls.Avalonia" />
<PackageReference Include="Svg.Controls.Skia.Avalonia" />
<PackageReference Include="DynamicData" />
@@ -69,6 +68,7 @@
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
<PackageReference Include="SPB" />
<PackageReference Include="SharpZipLib" />
</ItemGroup>
<ItemGroup>
@@ -169,8 +169,9 @@
<EmbeddedResource Include="Assets\UIImages\Logo_Amiibo.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Discord_Dark.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Discord_Light.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_GitLab_Dark.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_GitLab_Light.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Forgejo.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" />
</ItemGroup>
<ItemGroup>

View File

@@ -1234,9 +1234,17 @@ namespace Ryujinx.Ava.Systems
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());
if (_viewModel.IsActive)
if (_viewModel.IsActive && !hasModalFocusLoss)
{
bool isCursorVisible = true;
@@ -1369,7 +1377,7 @@ namespace Ryujinx.Ava.Systems
// Touchscreen.
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());
}

View File

@@ -13,6 +13,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Key = Ryujinx.Common.Configuration.Hid.Key;
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using RyuLogger = Ryujinx.Common.Logging.Logger;
namespace Ryujinx.Ava.Systems.Configuration
@@ -269,45 +271,45 @@ namespace Ryujinx.Ava.Systems.Configuration
Id = "0",
PlayerIndex = PlayerIndex.Player1,
ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<Key>
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{
DpadUp = Key.Up,
DpadDown = Key.Down,
DpadLeft = Key.Left,
DpadRight = Key.Right,
ButtonMinus = Key.Minus,
ButtonL = Key.E,
ButtonZl = Key.Q,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
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<Key>
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{
ButtonA = Key.Z,
ButtonB = Key.X,
ButtonX = Key.C,
ButtonY = Key.V,
ButtonPlus = Key.Plus,
ButtonR = Key.U,
ButtonZr = Key.O,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
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<Key>
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
},
}
];

View File

@@ -8,6 +8,8 @@ using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE;
using System;
using System.Linq;
using Key = Ryujinx.Common.Configuration.Hid.Key;
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Ava.Systems.Configuration
{
@@ -285,45 +287,45 @@ namespace Ryujinx.Ava.Systems.Configuration
Name = "Keyboard",
PlayerIndex = PlayerIndex.Player1,
ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<Key>
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{
DpadUp = Key.Up,
DpadDown = Key.Down,
DpadLeft = Key.Left,
DpadRight = Key.Right,
ButtonMinus = Key.Minus,
ButtonL = Key.E,
ButtonZl = Key.Q,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
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<Key>
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{
ButtonA = Key.Z,
ButtonB = Key.X,
ButtonX = Key.C,
ButtonY = Key.V,
ButtonPlus = Key.Plus,
ButtonR = Key.U,
ButtonZr = Key.O,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
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<Key>
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
},
}
];

View File

@@ -91,7 +91,7 @@ namespace Ryujinx.Ava.Systems
}
// If build URL not found, assume no new update is available.
if (string.IsNullOrEmpty(_versionResponse.ArtifactUrl))
if (_versionResponse.ArtifactUrl is null or "")
{
if (showVersionUpToDate)
{
@@ -123,8 +123,6 @@ namespace Ryujinx.Ava.Systems
return default;
}
_connectionCount = (int)_versionResponse.MaxConcurrency;
return (currentVersion, newVersion);
}
}

View File

@@ -1,20 +1,19 @@
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Gommon;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using SharpCompress.Archives;
using SharpCompress.Compressors.Xz;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Formats.Tar;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -22,6 +21,7 @@ using System.Net.NetworkInformation;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -32,8 +32,8 @@ namespace Ryujinx.Ava.Systems
private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory;
private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish");
private const int ConnectionCount = 4;
private static int _connectionCount = 1;
private static long _buildSize;
private static bool _updateSuccessful;
private static bool _running;
@@ -73,6 +73,27 @@ namespace Ryujinx.Ava.Systems
return;
}
// Fetch build size information to learn chunk sizes.
using HttpClient buildSizeClient = ConstructHttpClient();
try
{
buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
// GitLab instance is located in Ukraine. Connection times will vary across the world.
buildSizeClient.Timeout = TimeSpan.FromSeconds(10);
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_versionResponse.ArtifactUrl), HttpCompletionOption.ResponseHeadersRead);
_buildSize = message.Content.Headers.ContentRange.Length.Value;
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, ex.Message);
Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater");
_buildSize = -1;
}
await Dispatcher.UIThread.InvokeAsync(async () =>
{
string newVersionString = ReleaseInformation.IsCanaryBuild
@@ -122,14 +143,6 @@ namespace Ryujinx.Ava.Systems
Directory.CreateDirectory(_updateDir);
// If we get a .zip url switch it to the preferred .7z file instead
// The update server still returns the .zip url by default for legacy support
downloadUrl = downloadUrl.Replace(".zip", ".7z");
// If we get a .tar.gz url switch it to the preferred .tar.xz file instead
// The update server still returns the .tar.gz url by default for legacy support
downloadUrl = downloadUrl.Replace(".tar.gz", ".tar.xz");
string updateFile = Path.Combine(_updateDir, "update.bin");
TaskDialog taskDialog = new()
@@ -140,27 +153,6 @@ namespace Ryujinx.Ava.Systems
ShowProgressBar = true,
XamlRoot = RyujinxApp.MainWindow,
};
// Fetch build size information to learn chunk sizes.
using HttpClient buildSizeClient = ConstructHttpClient();
try
{
buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
// Forgejo instance is located in Ukraine. Connection times will vary across the world.
buildSizeClient.Timeout = TimeSpan.FromSeconds(10);
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_versionResponse.ArtifactUrl), HttpCompletionOption.ResponseHeadersRead);
_buildSize = message.Content.Headers.ContentRange.Length.Value;
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, ex.Message);
Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater");
_buildSize = -1;
}
taskDialog.Opened += (s, e) =>
{
@@ -242,22 +234,22 @@ namespace Ryujinx.Ava.Systems
private static void DoUpdateWithMultipleThreads(TaskDialog taskDialog, string downloadUrl, string updateFile)
{
// Multi-Threaded Updater
long chunkSize = _buildSize / _connectionCount;
long remainderChunk = _buildSize % _connectionCount;
long chunkSize = _buildSize / ConnectionCount;
long remainderChunk = _buildSize % ConnectionCount;
int completedRequests = 0;
int totalProgressPercentage = 0;
int[] progressPercentage = new int[_connectionCount];
int[] progressPercentage = new int[ConnectionCount];
List<byte[]> list = new(_connectionCount);
List<WebClient> webClients = new(_connectionCount);
List<byte[]> list = new(ConnectionCount);
List<WebClient> webClients = new(ConnectionCount);
for (int i = 0; i < _connectionCount; i++)
for (int i = 0; i < ConnectionCount; i++)
{
list.Add([]);
}
for (int i = 0; i < _connectionCount; i++)
for (int i = 0; i < ConnectionCount; i++)
{
#pragma warning disable SYSLIB0014
// TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
@@ -266,7 +258,7 @@ namespace Ryujinx.Ava.Systems
webClients.Add(client);
if (i == _connectionCount - 1)
if (i == ConnectionCount - 1)
{
client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}");
}
@@ -283,7 +275,7 @@ namespace Ryujinx.Ava.Systems
Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
taskDialog.SetProgressBarState(totalProgressPercentage / _connectionCount, TaskDialogProgressState.Normal);
taskDialog.SetProgressBarState(totalProgressPercentage / ConnectionCount, TaskDialogProgressState.Normal);
};
client.DownloadDataCompleted += (_, args) =>
@@ -302,10 +294,10 @@ namespace Ryujinx.Ava.Systems
list[index] = args.Result;
Interlocked.Increment(ref completedRequests);
if (Equals(completedRequests, _connectionCount))
if (Equals(completedRequests, ConnectionCount))
{
byte[] mergedFileBytes = new byte[_buildSize];
for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < _connectionCount; connectionIndex++)
for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++)
{
Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length);
destinationOffset += list[connectionIndex].Length;
@@ -410,33 +402,73 @@ namespace Ryujinx.Ava.Systems
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private static void ExtractTarGzipFile(string archivePath, string outputDirectoryPath)
private static void ExtractTarGzipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
{
using FileStream inStream = File.OpenRead(archivePath);
using GZipStream gzipStream = new(inStream, CompressionMode.Decompress);
TarFile.ExtractToDirectory(gzipStream, outputDirectoryPath, true);
}
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private static void ExtractTarXzipFile(string archivePath, string outputDirectoryPath)
{
using FileStream inStream = File.OpenRead(archivePath);
using XZStream gzipStream = new(inStream);
TarFile.ExtractToDirectory(gzipStream, outputDirectoryPath, true);
using GZipInputStream gzipStream = new(inStream);
using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
TarEntry tarEntry;
while ((tarEntry = tarStream.GetNextEntry()) is not null)
{
if (tarEntry.IsDirectory)
{
continue;
}
string outPath = Path.Combine(outputDirectoryPath, tarEntry.Name);
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
using FileStream outStream = File.OpenWrite(outPath);
tarStream.CopyEntryContents(outStream);
File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
Dispatcher.UIThread.Post(() =>
{
if (tarEntry is null)
{
return;
}
taskDialog.SetProgressBarState(GetPercentage(tarEntry.Size, inStream.Length), TaskDialogProgressState.Normal);
});
}
}
private static void ExtractZipFile(string archivePath, string outputDirectoryPath)
private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
{
ZipFile.ExtractToDirectory(archivePath, outputDirectoryPath);
}
private static void Extract7ZipFile(string archivePath, string outputDirectoryPath)
{
IArchive archive = ArchiveFactory.OpenArchive(archivePath);
archive.WriteToDirectory(outputDirectoryPath);
using Stream inStream = File.OpenRead(archivePath);
using ZipFile zipFile = new(inStream);
double count = 0;
foreach (ZipEntry zipEntry in zipFile)
{
count++;
if (zipEntry.IsDirectory)
{
continue;
}
string outPath = Path.Combine(outputDirectoryPath, zipEntry.Name);
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
using Stream zipStream = zipFile.GetInputStream(zipEntry);
using FileStream outStream = File.OpenWrite(outPath);
zipStream.CopyTo(outStream);
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
Dispatcher.UIThread.Post(() =>
{
taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
});
}
}
private static void InstallUpdate(TaskDialog taskDialog, string updateFile)
@@ -447,20 +479,16 @@ namespace Ryujinx.Ava.Systems
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
ExtractTarXzipFile(updateFile, _updateDir);
ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
}
else if (OperatingSystem.IsWindows())
{
Extract7ZipFile(updateFile, _updateDir);
ExtractZipFile(taskDialog, updateFile, _updateDir);
}
else
{
throw new NotSupportedException();
}
// The new decompression implementations don't have a way to show progress
// so the progressbar is just set to 100% after the decompression is done
taskDialog.SetProgressBarState(100, TaskDialogProgressState.Normal);
// Delete downloaded zip
File.Delete(updateFile);

View File

@@ -65,7 +65,7 @@ namespace Ryujinx.Ava.UI.Applet
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))
{
@@ -85,7 +85,7 @@ namespace Ryujinx.Ava.UI.Applet
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))
{

View File

@@ -1,5 +1,6 @@
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using Ryujinx.Ava.Input;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System;
@@ -25,6 +26,7 @@ namespace Ryujinx.Ava.UI.Helpers
private bool _isWaitingForInput;
private bool _shouldUnbind;
private IKeyboard _keyboard;
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
public ButtonKeyAssigner(ToggleButton toggleButton)
@@ -34,6 +36,9 @@ namespace Ryujinx.Ava.UI.Helpers
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
{
_keyboard = keyboard;
ClearKeyboardState(_keyboard);
Dispatcher.UIThread.Post(() =>
{
ToggledButton.IsChecked = true;
@@ -82,6 +87,7 @@ namespace Ryujinx.Ava.UI.Helpers
_isWaitingForInput = false;
ToggledButton.IsChecked = false;
ClearKeyboardState(_keyboard);
if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace)
{
@@ -98,6 +104,15 @@ namespace Ryujinx.Ava.UI.Helpers
_isWaitingForInput = false;
ToggledButton.IsChecked = false;
_shouldUnbind = shouldUnbind;
ClearKeyboardState(_keyboard);
}
private static void ClearKeyboardState(IKeyboard keyboard)
{
if (keyboard is AvaloniaKeyboard avaloniaKeyboard)
{
avaloniaKeyboard.Clear();
}
}
}
}

View File

@@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller;
using System;
using System.Collections.Generic;
using System.Globalization;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.UI.Helpers
{
@@ -12,79 +13,6 @@ namespace Ryujinx.Ava.UI.Helpers
{
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()
{
{ GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick },
@@ -110,49 +38,38 @@ namespace Ryujinx.Ava.UI.Helpers
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
{ GamepadInputId.Unbound, LocaleKeys.KeyUnbound},
{ GamepadInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
};
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
{
{ StickInputId.Left, LocaleKeys.StickLeft},
{ StickInputId.Right, LocaleKeys.StickRight},
{ StickInputId.Unbound, LocaleKeys.KeyUnbound},
{ StickInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
};
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string keyString = string.Empty;
LocaleKeys localeKey;
switch (value)
{
case Key key:
if (_keysMap.TryGetValue(key, out localeKey))
if (KeyboardLayoutLocaleHelper.TryGetSemanticLabel(key, out string localizedKeyLabel))
{
if (OperatingSystem.IsMacOS())
{
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];
keyString = localizedKeyLabel;
}
else
{
keyString = key.ToString();
}
break;
case PhysicalKey physicalKey:
keyString = PhysicalKeyLabelHelper.GetDisplayString(physicalKey);
break;
case GamepadInputId gamepadInputId:
LocaleKeys localeKey;
if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey))
{
keyString = LocaleManager.Instance[localeKey];

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

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

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

View File

@@ -13,88 +13,88 @@ namespace Ryujinx.Ava.UI.Models.Input
public PlayerIndex PlayerIndex { get; set; }
[ObservableProperty]
public partial Key LeftStickUp { get; set; }
public partial PhysicalKey LeftStickUp { get; set; }
[ObservableProperty]
public partial Key LeftStickDown { get; set; }
public partial PhysicalKey LeftStickDown { get; set; }
[ObservableProperty]
public partial Key LeftStickLeft { get; set; }
public partial PhysicalKey LeftStickLeft { get; set; }
[ObservableProperty]
public partial Key LeftStickRight { get; set; }
public partial PhysicalKey LeftStickRight { get; set; }
[ObservableProperty]
public partial Key LeftStickButton { get; set; }
public partial PhysicalKey LeftStickButton { get; set; }
[ObservableProperty]
public partial Key RightStickUp { get; set; }
public partial PhysicalKey RightStickUp { get; set; }
[ObservableProperty]
public partial Key RightStickDown { get; set; }
public partial PhysicalKey RightStickDown { get; set; }
[ObservableProperty]
public partial Key RightStickLeft { get; set; }
public partial PhysicalKey RightStickLeft { get; set; }
[ObservableProperty]
public partial Key RightStickRight { get; set; }
public partial PhysicalKey RightStickRight { get; set; }
[ObservableProperty]
public partial Key RightStickButton { get; set; }
public partial PhysicalKey RightStickButton { get; set; }
[ObservableProperty]
public partial Key DpadUp { get; set; }
public partial PhysicalKey DpadUp { get; set; }
[ObservableProperty]
public partial Key DpadDown { get; set; }
public partial PhysicalKey DpadDown { get; set; }
[ObservableProperty]
public partial Key DpadLeft { get; set; }
public partial PhysicalKey DpadLeft { get; set; }
[ObservableProperty]
public partial Key DpadRight { get; set; }
public partial PhysicalKey DpadRight { get; set; }
[ObservableProperty]
public partial Key ButtonMinus { get; set; }
public partial PhysicalKey ButtonMinus { get; set; }
[ObservableProperty]
public partial Key ButtonPlus { get; set; }
public partial PhysicalKey ButtonPlus { get; set; }
[ObservableProperty]
public partial Key ButtonA { get; set; }
public partial PhysicalKey ButtonA { get; set; }
[ObservableProperty]
public partial Key ButtonB { get; set; }
public partial PhysicalKey ButtonB { get; set; }
[ObservableProperty]
public partial Key ButtonX { get; set; }
public partial PhysicalKey ButtonX { get; set; }
[ObservableProperty]
public partial Key ButtonY { get; set; }
public partial PhysicalKey ButtonY { get; set; }
[ObservableProperty]
public partial Key ButtonL { get; set; }
public partial PhysicalKey ButtonL { get; set; }
[ObservableProperty]
public partial Key ButtonR { get; set; }
public partial PhysicalKey ButtonR { get; set; }
[ObservableProperty]
public partial Key ButtonZl { get; set; }
public partial PhysicalKey ButtonZl { get; set; }
[ObservableProperty]
public partial Key ButtonZr { get; set; }
public partial PhysicalKey ButtonZr { get; set; }
[ObservableProperty]
public partial Key LeftButtonSl { get; set; }
public partial PhysicalKey LeftButtonSl { get; set; }
[ObservableProperty]
public partial Key LeftButtonSr { get; set; }
public partial PhysicalKey LeftButtonSr { get; set; }
[ObservableProperty]
public partial Key RightButtonSl { get; set; }
public partial PhysicalKey RightButtonSl { get; set; }
[ObservableProperty]
public partial Key RightButtonSr { get; set; }
public partial PhysicalKey RightButtonSr { get; set; }
public KeyboardInputConfig(InputConfig config)
{
@@ -153,7 +153,7 @@ namespace Ryujinx.Ava.UI.Models.Input
Backend = InputBackendType.WindowKeyboard,
PlayerIndex = PlayerIndex,
ControllerType = ControllerType,
LeftJoycon = new LeftJoyconCommonConfig<Key>
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{
DpadUp = DpadUp,
DpadDown = DpadDown,
@@ -165,7 +165,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ButtonSl = LeftButtonSl,
ButtonSr = LeftButtonSr,
},
RightJoycon = new RightJoyconCommonConfig<Key>
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{
ButtonA = ButtonA,
ButtonB = ButtonB,
@@ -177,7 +177,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ButtonR = ButtonR,
ButtonZr = ButtonZr,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = LeftStickUp,
StickDown = LeftStickDown,
@@ -185,7 +185,7 @@ namespace Ryujinx.Ava.UI.Models.Input
StickLeft = LeftStickLeft,
StickButton = LeftStickButton,
},
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = RightStickUp,
StickDown = RightStickDown,
@@ -198,5 +198,37 @@ namespace Ryujinx.Ava.UI.Models.Input
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));
}
}
}

View File

@@ -154,42 +154,42 @@ namespace Ryujinx.Ava.UI.Models.Input
{
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
if (snapshot.IsPressed(KeyboardConfig.LeftStickRight))
{
leftBuffer.Item1 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
if (snapshot.IsPressed(KeyboardConfig.LeftStickLeft))
{
leftBuffer.Item1 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
if (snapshot.IsPressed(KeyboardConfig.LeftStickUp))
{
leftBuffer.Item2 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
if (snapshot.IsPressed(KeyboardConfig.LeftStickDown))
{
leftBuffer.Item2 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
if (snapshot.IsPressed(KeyboardConfig.RightStickRight))
{
rightBuffer.Item1 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
if (snapshot.IsPressed(KeyboardConfig.RightStickLeft))
{
rightBuffer.Item1 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
if (snapshot.IsPressed(KeyboardConfig.RightStickUp))
{
rightBuffer.Item2 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
if (snapshot.IsPressed(KeyboardConfig.RightStickDown))
{
rightBuffer.Item2 -= 1;
}

View File

@@ -45,7 +45,6 @@ namespace Ryujinx.Ava.UI.Renderer
Content = EmbeddedWindow;
}
public void Dispose()
{
if (EmbeddedWindow != null)

View File

@@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
public partial class AboutWindowViewModel : BaseModel, IDisposable
{
[ObservableProperty] public partial Bitmap ForgejoLogo { get; set; }
[ObservableProperty] public partial Bitmap GitLabLogo { get; set; }
[ObservableProperty] public partial Bitmap DiscordLogo { get; set; }
@@ -37,7 +37,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
private const string LogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}_{1}.png?assembly=Ryujinx";
private const string UnthemedLogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}.png?assembly=Ryujinx";
private void UpdateLogoTheme(string theme)
{
@@ -47,7 +46,7 @@ namespace Ryujinx.Ava.UI.ViewModels
string themeName = isDarkTheme ? "Dark" : "Light";
DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
ForgejoLogo = LoadBitmap(UnthemedLogoPathFormat.Format("Forgejo"));
GitLabLogo = LoadBitmap(LogoPathFormat.Format("GitLab", themeName));
}
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
@@ -56,7 +55,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
ForgejoLogo.Dispose();
GitLabLogo.Dispose();
DiscordLogo.Dispose();
GC.SuppressFinalize(this);

View File

@@ -88,19 +88,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public async void ShowMotionConfig()
{
await MotionInputView.Show(this);
ParentModel.IsModified = true;
ParentModel.RefreshModifiedState();
}
public async void ShowRumbleConfig()
{
await RumbleInputView.Show(this);
ParentModel.IsModified = true;
ParentModel.RefreshModifiedState();
}
public async void ShowLedConfig()
{
await LedInputView.Show(this);
ParentModel.IsModified = true;
ParentModel.RefreshModifiedState();
}
public void OnParentModelChanged()

View File

@@ -1,6 +1,7 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Svg.Skia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using Ryujinx.Ava.Common.Locale;
@@ -28,7 +29,7 @@ using System.Linq;
using System.Text.Json;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
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
{
@@ -42,6 +43,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private const string KeyboardString = "keyboard";
private const string ControllerString = "controller";
private readonly MainWindow _mainWindow;
private Control _keyboardDriverControl;
private PlayerIndex _playerId;
private PlayerIndex _playerIdChoose;
@@ -65,7 +67,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public IGamepadDriver AvaloniaKeyboardDriver { get; }
public IGamepadDriver AvaloniaKeyboardDriver { get; private set; }
public IGamepad SelectedGamepad
{
@@ -89,7 +91,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
internal ObservableCollection<ControllerModel> Controllers { get; set; }
public AvaloniaList<string> ProfilesList { get; set; }
public AvaloniaList<string> DeviceList { get; set; }
public bool UseGlobalConfig;
@@ -99,7 +100,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool IsKeyboard => !IsController;
public bool IsRight { get; set; }
public bool IsLeft { get; set; }
public string RevertDeviceId { get; set; }
public bool HasLed => (SelectedGamepad.Features & GamepadFeaturesFlag.Led) != 0;
public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense");
@@ -163,7 +163,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
LoadDevice();
LoadProfiles();
RevertDeviceId = Devices[Device].Id;
_isLoaded = true;
_isChangeTrackingActive = true;
OnPropertyChanged();
@@ -175,52 +174,58 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
get => _controller;
set
{
MarkAsChanged();
int controllerIndex = value < 0 ? 0 : value;
_controller = value;
if (_controller == -1)
if (controllerIndex == _controller)
{
_controller = 0;
return;
}
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();
NotifyChanges();
ApplyControllerSelection(controllerIndex);
RefreshModifiedState();
}
}
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
{
get => _controllerImage;
@@ -255,33 +260,83 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
get => _device;
set
{
MarkAsChanged();
_device = value < 0 ? 0 : value;
if (_device >= Devices.Count)
if (value < 0 || value >= Devices.Count)
{
return;
}
_device = value;
DeviceType selected = Devices[_device].Type;
if (selected != DeviceType.None)
{
LoadControllers();
if (_isLoaded)
{
LoadConfiguration(LoadDefaultConfiguration());
LoadSelectedDeviceDefaults();
}
else
{
LoadSelectedDeviceControllers();
}
}
RefreshModifiedState();
FindPairedDeviceInConfigFile();
OnPropertyChanged();
OnPropertyChanged(nameof(SelectedDeviceItem));
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 InputViewModel(UserControl owner, bool useGlobal = false) : this()
@@ -290,7 +345,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_mainWindow = RyujinxApp.MainWindow;
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner);
ReplaceKeyboardDriver(owner);
PhysicalKeyLabelHelper.LabelsChanged += OnPhysicalKeyLabelsChanged;
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -301,7 +357,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_isLoaded = false;
LoadDevices();
RefreshAvailableDevices();
PlayerId = PlayerIndex.Player1;
}
@@ -309,13 +365,22 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_isChangeTrackingActive = true;
}
public void RetargetKeyboardDriver(Control owner)
{
if (!Program.PreviewerDetached)
{
return;
}
ReplaceKeyboardDriver(owner);
}
public InputViewModel()
{
PlayerIndexes = [];
Controllers = [];
Devices = [];
ProfilesList = [];
DeviceList = [];
VisualStick = new StickVisualizer(this);
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)
{
Config = inputConfig ?? 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.InstanceExtra.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)
{
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()
{
// 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()
{
// "Disabled" mode is available after unbinding the device
@@ -395,34 +465,117 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public void LoadDevice()
{
int deviceIndex = 0;
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;
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;
}
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)
{
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()
@@ -462,11 +615,24 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_isChangeTrackingActive = false; // Disable configuration change tracking
LoadDevices();
bool shouldApplyKeyboardFallback = Config is StandardControllerInputConfig controllerConfig && controllerConfig.Id == id;
IsModified = true;
RevertChanges();
FindPairedDeviceInConfigFile();
RefreshAvailableDevices();
if (shouldApplyKeyboardFallback)
{
LoadConfiguration();
LoadDevice();
NotificationIsVisible = false;
IsModified = false;
NotifyChanges();
}
else
{
IsModified = true;
RevertChanges();
FindPairedDeviceInConfigFile();
}
_isChangeTrackingActive = true; // Enable configuration change tracking
@@ -476,7 +642,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_isChangeTrackingActive = false; // Disable configuration change tracking
LoadDevices();
RefreshAvailableDevices();
IsModified = true;
RevertChanges();
@@ -502,6 +668,23 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
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()
{
Controllers.Clear();
@@ -510,7 +693,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeHandheld]));
Controller = 0;
ApplyControllerSelection(0);
}
else
{
@@ -528,14 +711,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
// Workaround: set the box to 1 and then 0
if (controllerIndex == 0)
{
Controller = 1;
ApplyControllerSelection(1);
}
Controller = controllerIndex;
ApplyControllerSelection(controllerIndex);
}
else
{
Controller = 0;
ApplyControllerSelection(0);
}
}
}
@@ -561,8 +744,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
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)
{
return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
@@ -583,7 +774,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
lock (Devices)
{
Devices.Clear();
DeviceList.Clear();
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));
Device = Math.Min(Device, DeviceList.Count);
if (selectedDevice != default)
{
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()
@@ -677,46 +878,46 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
Id = id,
Name = name,
ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<Key>
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{
DpadUp = Key.Up,
DpadDown = Key.Down,
DpadLeft = Key.Left,
DpadRight = Key.Right,
ButtonMinus = Key.Minus,
ButtonL = Key.E,
ButtonZl = Key.Q,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
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<Key>
new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{
ButtonA = Key.Z,
ButtonB = Key.X,
ButtonX = Key.C,
ButtonY = Key.V,
ButtonPlus = Key.Plus,
ButtonR = Key.U,
ButtonZr = Key.O,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
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<Key>
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
},
};
}
@@ -860,7 +1061,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_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);
@@ -958,9 +1166,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
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;
LoadConfiguration();
LoadDevice();
@@ -980,8 +1185,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
IsModified = false;
RevertDeviceId = Devices[Device].Id; // Remember selected device after saving
List<InputConfig> newConfig = [];
if (UseGlobalConfig && Program.UseExtraConfig)
@@ -1001,25 +1204,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
else
{
(DeviceType Type, string Id, string Name) device = Devices[Device];
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;
InputConfig config = GetSelectedDeviceConfig();
int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId);
if (i == -1)
@@ -1060,9 +1245,46 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
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()
{
GC.SuppressFinalize(this);
PhysicalKeyLabelHelper.LabelsChanged -= OnPhysicalKeyLabelsChanged;
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
@@ -1073,7 +1295,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
SelectedGamepad?.Dispose();
AvaloniaKeyboardDriver.Dispose();
AvaloniaKeyboardDriver?.Dispose();
}
}
}

View File

@@ -122,8 +122,8 @@
Click="Button_OnClick"
CornerRadius="15"
Tag="https://src.ryujinx.app"
ToolTip.Tip="{ext:Locale AboutForgejoUrlTooltipMessage}">
<Image Source="{Binding ForgejoLogo}" />
ToolTip.Tip="{ext:Locale AboutGitLabUrlTooltipMessage}">
<Image Source="{Binding GitLabLogo}" />
</Button>
<Button
MinWidth="30"

View File

@@ -116,7 +116,6 @@ namespace Ryujinx.Ava.UI.Views.Input
if (e.ButtonValue.HasValue)
{
Button buttonValue = e.ButtonValue.Value;
FlagInputConfigChanged();
switch (button.Name)
{
@@ -187,6 +186,8 @@ namespace Ryujinx.Ava.UI.Views.Input
viewModel.Config.RightJoystick = buttonValue.AsHidType<StickInputId>();
break;
}
FlagInputConfigChanged();
}
};
@@ -212,7 +213,7 @@ namespace Ryujinx.Ava.UI.Views.Input
private void FlagInputConfigChanged()
{
(DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true;
(DataContext as ControllerInputViewModel)!.ParentModel.RefreshModifiedState();
}
private void MouseClick(object sender, PointerPressedEventArgs e)

View File

@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
@@ -148,7 +149,7 @@
<Grid
Grid.Column="0"
Margin="2"
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto">
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock
Grid.Column="0"
Margin="5,0,10,0"
@@ -161,19 +162,38 @@
Name="DeviceBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ItemsSource="{Binding DeviceList}"
SelectedIndex="{Binding Device}" />
ItemsSource="{Binding Devices}"
SelectedItem="{Binding SelectedDeviceItem, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ., Converter={x:Static helpers:InputDeviceNameConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button
Grid.Column="2"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
Command="{Binding LoadDevice}">
ToolTip.Tip="{ext:Locale ControllerSettingsRefresh}"
Command="{Binding RefreshInputDevices}">
<ui:SymbolIcon
Symbol="Refresh"
FontSize="15"
Height="20"/>
</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>
<!-- Controller Type -->
<Grid

View File

@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
@@ -15,9 +16,14 @@ namespace Ryujinx.Ava.UI.Views.Input
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()
@@ -28,8 +34,18 @@ namespace Ryujinx.Ava.UI.Views.Input
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
{
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();
if (VisualRoot is not null)
{
ViewModel.RetargetKeyboardDriver(this);
}
}
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)

View File

@@ -12,7 +12,7 @@ using Ryujinx.Input.Assigner;
using System;
using System.Collections.Generic;
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
{
@@ -73,95 +73,96 @@ namespace Ryujinx.Ava.UI.Views.Input
if (be.ButtonValue.HasValue)
{
Button buttonValue = be.ButtonValue.Value;
ViewModel.ParentModel.IsModified = true;
switch (button.Name)
{
case "ButtonZl":
ViewModel.Config.ButtonZl = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonZl = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonL":
ViewModel.Config.ButtonL = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonL = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonMinus":
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickButton":
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickUp":
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickDown":
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickRight":
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickLeft":
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadUp":
ViewModel.Config.DpadUp = buttonValue.AsHidType<Key>();
ViewModel.Config.DpadUp = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadDown":
ViewModel.Config.DpadDown = buttonValue.AsHidType<Key>();
ViewModel.Config.DpadDown = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadLeft":
ViewModel.Config.DpadLeft = buttonValue.AsHidType<Key>();
ViewModel.Config.DpadLeft = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadRight":
ViewModel.Config.DpadRight = buttonValue.AsHidType<Key>();
ViewModel.Config.DpadRight = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftButtonSr":
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftButtonSl":
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightButtonSr":
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<Key>();
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightButtonSl":
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<Key>();
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonZr":
ViewModel.Config.ButtonZr = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonZr = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonR":
ViewModel.Config.ButtonR = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonR = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonPlus":
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonA":
ViewModel.Config.ButtonA = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonA = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonB":
ViewModel.Config.ButtonB = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonB = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonX":
ViewModel.Config.ButtonX = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonX = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonY":
ViewModel.Config.ButtonY = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonY = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickButton":
ViewModel.Config.RightStickButton = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickButton = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickUp":
ViewModel.Config.RightStickUp = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickUp = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickDown":
ViewModel.Config.RightStickDown = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickDown = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickRight":
ViewModel.Config.RightStickRight = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickRight = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickLeft":
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<PhysicalKey>();
break;
}
ViewModel.ParentModel.RefreshModifiedState();
}
};
@@ -207,40 +208,40 @@ namespace Ryujinx.Ava.UI.Views.Input
{
Dictionary<string, Action> buttonActions = new()
{
{ "ButtonZl", () => ViewModel.Config.ButtonZl = Key.Unbound },
{ "ButtonL", () => ViewModel.Config.ButtonL = Key.Unbound },
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = Key.Unbound },
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = Key.Unbound },
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = Key.Unbound },
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = Key.Unbound },
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = Key.Unbound },
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = Key.Unbound },
{ "DpadUp", () => ViewModel.Config.DpadUp = Key.Unbound },
{ "DpadDown", () => ViewModel.Config.DpadDown = Key.Unbound },
{ "DpadLeft", () => ViewModel.Config.DpadLeft = Key.Unbound },
{ "DpadRight", () => ViewModel.Config.DpadRight = Key.Unbound },
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = Key.Unbound },
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = Key.Unbound },
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = Key.Unbound },
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = Key.Unbound },
{ "ButtonZr", () => ViewModel.Config.ButtonZr = Key.Unbound },
{ "ButtonR", () => ViewModel.Config.ButtonR = Key.Unbound },
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = Key.Unbound },
{ "ButtonA", () => ViewModel.Config.ButtonA = Key.Unbound },
{ "ButtonB", () => ViewModel.Config.ButtonB = Key.Unbound },
{ "ButtonX", () => ViewModel.Config.ButtonX = Key.Unbound },
{ "ButtonY", () => ViewModel.Config.ButtonY = Key.Unbound },
{ "RightStickButton", () => ViewModel.Config.RightStickButton = Key.Unbound },
{ "RightStickUp", () => ViewModel.Config.RightStickUp = Key.Unbound },
{ "RightStickDown", () => ViewModel.Config.RightStickDown = Key.Unbound },
{ "RightStickRight", () => ViewModel.Config.RightStickRight = Key.Unbound },
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = Key.Unbound }
{ "ButtonZl", () => ViewModel.Config.ButtonZl = PhysicalKey.Unbound },
{ "ButtonL", () => ViewModel.Config.ButtonL = PhysicalKey.Unbound },
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = PhysicalKey.Unbound },
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = PhysicalKey.Unbound },
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = PhysicalKey.Unbound },
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = PhysicalKey.Unbound },
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = PhysicalKey.Unbound },
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = PhysicalKey.Unbound },
{ "DpadUp", () => ViewModel.Config.DpadUp = PhysicalKey.Unbound },
{ "DpadDown", () => ViewModel.Config.DpadDown = PhysicalKey.Unbound },
{ "DpadLeft", () => ViewModel.Config.DpadLeft = PhysicalKey.Unbound },
{ "DpadRight", () => ViewModel.Config.DpadRight = PhysicalKey.Unbound },
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = PhysicalKey.Unbound },
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = PhysicalKey.Unbound },
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = PhysicalKey.Unbound },
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = PhysicalKey.Unbound },
{ "ButtonZr", () => ViewModel.Config.ButtonZr = PhysicalKey.Unbound },
{ "ButtonR", () => ViewModel.Config.ButtonR = PhysicalKey.Unbound },
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = PhysicalKey.Unbound },
{ "ButtonA", () => ViewModel.Config.ButtonA = PhysicalKey.Unbound },
{ "ButtonB", () => ViewModel.Config.ButtonB = PhysicalKey.Unbound },
{ "ButtonX", () => ViewModel.Config.ButtonX = PhysicalKey.Unbound },
{ "ButtonY", () => ViewModel.Config.ButtonY = PhysicalKey.Unbound },
{ "RightStickButton", () => ViewModel.Config.RightStickButton = PhysicalKey.Unbound },
{ "RightStickUp", () => ViewModel.Config.RightStickUp = PhysicalKey.Unbound },
{ "RightStickDown", () => ViewModel.Config.RightStickDown = PhysicalKey.Unbound },
{ "RightStickRight", () => ViewModel.Config.RightStickRight = PhysicalKey.Unbound },
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = PhysicalKey.Unbound }
};
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
{
action();
ViewModel.ParentModel.IsModified = true;
ViewModel.ParentModel.RefreshModifiedState();
}
}
}

View File

@@ -295,17 +295,17 @@
<MenuItem
Name="SetupGuideMenuItem"
Header="{ext:Locale MenuBarHelpSetup}"
Icon="{ext:Icon fa-solid fa-circle-info}"
Icon="{ext:Icon fa-brands fa-gitlab}"
CommandParameter="{x:Static common:SharedConstants.SetupGuideWikiUrl}" />
<MenuItem
Name="LdnGuideMenuItem"
Header="{ext:Locale MenuBarHelpMultiplayer}"
Icon="{ext:Icon fa-solid fa-circle-info}"
Icon="{ext:Icon fa-brands fa-gitlab}"
CommandParameter="{x:Static common:SharedConstants.MultiplayerWikiUrl}" />
<MenuItem
Name="FaqMenuItem"
Header="{ext:Locale MenuBarHelpFaq}"
Icon="{ext:Icon fa-solid fa-circle-info}"
Icon="{ext:Icon fa-brands fa-gitlab}"
CommandParameter="{x:Static common:SharedConstants.FaqWikiUrl}" />
</MenuItem>
<Separator />

View File

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

View File

@@ -30,6 +30,7 @@ using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL3;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -105,7 +106,9 @@ namespace Ryujinx.Ava.UI.Windows
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.ScalingChanged += OnScalingChanged;