Compare commits

..

31 Commits

Author SHA1 Message Date
Max
93c76a5839 (Security) Updated to Avalonia 11.3.14 to bump Tmds.DBus.Protocol to 0.21.3 (!39)
https://github.com/tmds/Tmds.DBus/releases/tag/rel%2F0.21.3
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/39
2026-04-27 20:04:34 +00:00
KeatonTheBot
0040f884d4 Update SDL3-CS (!37)
This is a new package with unneeded architectures removed and binaries slimmed down (stripped symbols and some unused drivers). Plus, we can update the native binaries as needed and not wait on ppy.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/37
2026-04-27 12:33:46 +00:00
VewDev
bd3b147002 fix: broken updater (!35)
Fix an error where the updater doesn't work by throwing a missing method exception.

This is caused by the UpdateClient package not being bumped along with the Common one, causing a mismatch in methods.

Fixes https://github.com/Ryubing/Issues/issues/358

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/35
2026-04-25 11:48:52 +00:00
LotP
b3ac7a2b94 fix arg parsing (!34)
fixes parsing of args with spaces

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/34
2026-04-24 23:08:29 +00:00
LotP
96f8d519e6 fix hide console breaking on windows 11 (!33)
force the emulator to launch in an old-school cmd on windows

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/33
2026-04-24 15:42:41 +00:00
LotP
433dd58f8c revert 578b6bf00e
revert revert 9905ee17c2

Re-add missing solution configurations for Ryujinx.Audio.Backends.Apple (#10)

The original revert was made for testing purposes, so it is being rereverted.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/10
2026-04-22 21:39:57 +00:00
LotP
4347afff77 fix spelling mistake (#30)
mistakes were made

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/30
2026-04-22 21:21:17 +00:00
LotP
46f5d1d31a updated decompression support (#26)
- update decompression functions to use native libs
- add support for 7zip and tar.xz archives with SharpCompress
- add .7z and tar.xz artifacts to releases
- remove linux non-appimage builds from PRs

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/26
2026-04-22 21:13:34 +00:00
LotP
578b6bf00e revert 9905ee17c2
revert Add missing solution configurations for Ryujinx.Audio.Backends.Apple (#10)

Adds all missing solution configurations for Ryujinx.Audio.Backends.Apple. These somehow got overlooked.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/10
2026-04-20 21:52:42 +00:00
KeatonTheBot
9905ee17c2 Add missing solution configurations for Ryujinx.Audio.Backends.Apple (#10)
Adds all missing solution configurations for Ryujinx.Audio.Backends.Apple. These somehow got overlooked.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/10
2026-04-19 01:35:23 +00:00
Schuay
7f0e82fe48 fix/sdl3-gamepad-bugs (#19)
Four SDL3 gamepad fixes, all affecting real users with Switch controllers.

**Crash on reconnect / broken motion** — `GetFeaturesFlag()` was calling `SDL_DestroyProperties()` on properties owned by SDL internally, causing a use-after-free. This breaks gyro after the first read and crashes on reconnect. Fix: just don't destroy them.

**Thread safety in `GetGamepads()`** — was locking the wrong objects and holding locks across `yield` boundaries, which is undefined behavior with `System.Threading.Lock`. Fix: snapshot IDs under `_lock`, iterate outside it.

**Rumble NPE on disconnect** — `UpdateRumble()` called `_gamepad.Rumble()` without a null check; throws if the controller disconnects mid-update. Fix: `_gamepad?.Rumble(...)`.

**Linux: HIDAPI conflicts with `hid_nintendo`** — when the `hid_nintendo` kernel module is loaded, it provides evdev nodes for both the gamepad and IMU that SDL3's evdev backend correctly combines. HIDAPI conflicts with it, breaking gyro and wireless hotplug. Fix: detect `/sys/module/hid_nintendo` at startup and disable HIDAPI for Switch controllers automatically. Overridable via `SDL_JOYSTICK_HIDAPI_SWITCH=1`.

Tested with an 8BitDo Ultimate Bluetooth Controller on Linux.

Co-authored-by: schuay <36006+schuay@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/19
2026-04-18 13:54:25 +00:00
Max
69c22a744e [HID] Updated SDL3 package from 2025.920.0 -> 2026.320.0 (#9)
Should provide support for Nintendo Switch 2 controllers, new Steam hardware (controller, frame), and potentially [Switch as a gamepad via hekate](https://github.com/CTCaer/hekate/blob/master/bdk/usb/usb_gadget_hid.c).

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/9
2026-04-17 23:56:15 +00:00
Xam
35b1531102 HLE: CaptureManager: SaveScreenShot: properly handle screenshot image data (#18)
no way this was working before, and if it did, just pure luck, unsafe blind copy of bytes as is and zero checks.

i only tested tomodachi, but should fix all games that were crashing on saving screenshots

the crash was happening because the screenshot buffer was bigger than the bitmap buffer, so marshall.copy() was raising an unhandhled expection crashing the emu.

on top of this, because the data was just copied as is, the result image was garbled.

[fixes #304](https://github.com/Ryubing/Issues/issues/304)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/18
2026-04-17 23:08:33 +00:00
ryuadmin
ea1185e96a damnit 2026-04-17 22:00:49 +00:00
ryuadmin
31e0430b06 Rework labeler.yml for different label names and differing project structure 2026-04-17 21:59:57 +00:00
WilliamWsyHK
efb8658194 Update zh-tw Translations (#17)
Co-authored-by: WilliamWsyHK <WilliamWsyHK@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/17
2026-04-17 21:44:26 +00:00
ryuadmin
e909ea3210 bump labeler action to v6 2026-04-16 06:24:51 +00:00
sh0inx
2a688abeec Merge pull request 'i18n : New Translation for French (fr_FR )and Spanish (es_ES)' (#14) from Babib3l/Ryubing:ryujinx-Fr-Es-Translations into master
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/14
2026-04-15 20:28:26 +00:00
Babib3l
9f88d68268 fix: drop stale GitLab locale rebase leftovers 2026-04-14 00:27:54 +02:00
Babib3l
fe685c3f8f Rebase locale updates for GitLab and FR/ES translations 2026-04-14 00:27:46 +02:00
GreemDev
2edc165e26 chore: post-migration cleanup 2026-04-13 00:51:10 -05:00
ryuadmin
fa7cb22240 fix: probably gonna be chasing these down for a while 2026-04-12 20:55:02 +00:00
greem
c5b4add186 Maybe fix the labeler? 2026-04-12 17:59:20 +00:00
greem
0079e0bfa5 Add distribution/macos/create_macos_pr_build_ava.sh (#3)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/3
2026-04-12 03:11:13 +00:00
Shyanne
25b70e05d7 Update BiquadFilterEffectParameterTests.cs 2026-04-11 22:34:03 -04:00
ryuadmin
78eb95733a Update README.md 2026-04-12 00:17:13 +00:00
ryuadmin
17a768a5df Update .forgejo/workflows/canary.yml 2026-04-11 23:39:44 +00:00
ryuadmin
339cddd0e2 fix: extract-and-run AppImageTool in CI 2026-04-11 23:33:46 +00:00
ryuadmin
66d9bc5a35 how did this happen lol 2026-04-11 23:22:45 +00:00
ryuadmin
3e393449aa fix nuget sources post-migration 2026-04-11 23:16:35 +00:00
GreemDev
744c41937b feature: Forgejo Actions 2026-04-11 18:10:31 -05:00
80 changed files with 3320 additions and 4676 deletions

View File

@@ -10,6 +10,10 @@ 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/**'
@@ -18,17 +22,17 @@ gpu:
- 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.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**']
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.LocaleGenerator/**']
horizon:
'horizon/hle':
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.HLE/**', 'src/Ryujinx.Horizon/**']
- 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/**']
kernel:
- changed-files:
@@ -36,7 +40,7 @@ kernel:
infra:
- changed-files:
- any-glob-to-any-file: ['.github/**', 'distribution/**', 'Directory.Packages.props', 'src/Ryujinx.BuildValidationTasks/**']
- any-glob-to-any-file: ['.forgejo/**', 'distribution/**', 'Directory.Packages.props', 'src/Ryujinx.BuildValidationTasks/**']
documentation:
- changed-files:
@@ -44,4 +48,4 @@ documentation:
ldn:
- changed-files:
- any-glob-to-any-file: 'src/Ryujinx.HLE/HOS/Services/Ldn/**'
- any-glob-to-any-file: ['src/Ryujinx.HLE/HOS/Services/Ldn/**', 'src/Ryujinx/UI/Windows/LdnGamesListWindow.*', 'src/Ryujinx/UI/ViewModels/LdnGamesListViewModel.cs']

View File

@@ -0,0 +1,204 @@
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'

View File

@@ -6,7 +6,7 @@ on:
push:
branches: [ master ]
paths-ignore:
- '.github/**'
- '.forgejo/**'
- 'docs/**'
- 'assets/**'
- '*.yml'
@@ -25,44 +25,41 @@ env:
jobs:
release:
name: Release for ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
runs-on: docker
container:
image: ${{ matrix.platform.os }}
strategy:
matrix:
platform:
- { 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 }
- { 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 }
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.github/csc.json"
run: echo "::add-matcher::.forgejo/csc.json"
- name: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Install 7zip
run: |
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 }}
sudo apt update && sudo apt install -y 7zip
- name: Get version info
id: version_info
run: |
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
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
shell: bash
- name: Configure for release
@@ -87,12 +84,9 @@ 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')
@@ -101,9 +95,8 @@ 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)
@@ -112,7 +105,7 @@ jobs:
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
PLATFORM_NAME="${{ matrix.platform.name }}"
sudo apt install -y zsync desktop-file-utils appstream
sudo apt update && sudo apt install -y zsync desktop-file-utils appstream libfuse2t64
mkdir -p tools
export PATH="$PATH:$(readlink -f tools)"
@@ -139,17 +132,28 @@ 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: ubuntu-24.04
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
@@ -159,33 +163,24 @@ jobs:
chmod +x llvm.sh
sudo ./llvm.sh 17
- 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 GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- 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'
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
mv rcodesign $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
mv rcodesign /usr/bin/rcodesign
- name: Get version info
id: version_info
run: |
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
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
shell: bash
- name: Configure for release
@@ -201,46 +196,53 @@ 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
create_gitlab_release:
name: Create GitLab Release
runs-on: ubuntu-24.04
- 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
needs:
- macos_release
- release
steps:
- uses: actions/checkout@v4
- 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 GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- name: Get version info
id: version_info
run: |
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
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
shell: bash
- name: Create tag
run: |
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 }})"
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 }}
- name: Send notification webhook
run: |
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
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
- name: Notify update server of new builds
run: |

View File

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

View File

@@ -19,18 +19,20 @@ env:
jobs:
release:
name: Release for ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
runs-on: docker
container:
image: ${{ matrix.platform.os }}
strategy:
matrix:
platform:
- { 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 }
- { 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 }
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
@@ -41,26 +43,21 @@ jobs:
run: |
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: Install GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_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)" >> $GITHUB_OUTPUT
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $FORGEJO_OUTPUT
else
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
echo "build_version=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
fi
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 "prev_build_version=$(gli get-current-version -c Stable -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
shell: bash
- name: Configure for release
@@ -84,12 +81,9 @@ 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')
@@ -98,12 +92,9 @@ 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')
@@ -138,17 +129,27 @@ jobs:
pushd publish_appimage
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
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-$BUILD_VERSION-$ARCH_NAME.AppImage
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/**
macos_release:
name: Release MacOS universal
runs-on: ubuntu-24.04
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v4
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
@@ -158,37 +159,28 @@ jobs:
chmod +x llvm.sh
sudo ./llvm.sh 17
- 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 GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_TOKEN }}
- 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'
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
mv rcodesign $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
mv rcodesign /usr/bin/rcodesign
- 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)" >> $GITHUB_OUTPUT
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $FORGEJO_OUTPUT
else
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
echo "build_version=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
fi
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 "prev_build_version=$(gli get-current-version -c Stable -R)" >> $FORGEJO_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
shell: bash
- name: Configure for release
@@ -201,12 +193,20 @@ jobs:
- name: Publish macOS Ryujinx
run: |
./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
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
runs-on: ubuntu-24.04
needs:
- macos_release
@@ -214,36 +214,26 @@ jobs:
steps:
- uses: actions/checkout@v4
- 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 GLI
uses: actions/setup-gli@v1
with:
token: ${{ secrets.SETUP_GLI_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)" >> $GITHUB_OUTPUT
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $FORGEJO_OUTPUT
else
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
echo "build_version=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
fi
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
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
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.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
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
- name: Notify update server of new builds
run: |

View File

@@ -1,86 +0,0 @@
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

View File

@@ -1,5 +0,0 @@
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

@@ -1,31 +0,0 @@
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

@@ -1,26 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,168 +0,0 @@
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

@@ -1,25 +0,0 @@
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

3
.gitignore vendored
View File

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

View File

@@ -3,11 +3,12 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.12" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.12" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.12" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.12" />
<PackageVersion Include="Avalonia" Version="11.3.14" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.14" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.14" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.14" />
<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" />
@@ -16,7 +17,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="ppy.SDL3-CS" Version="2025.920.0" />
<PackageVersion Include="Ryujinx.SDL3-CS" Version="2026.426.0" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="Concentus" Version="2.2.2" />
@@ -42,13 +43,12 @@
<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="1.0.44" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="2.0.6" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="2.0.6" />
<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://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)
[![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)
<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 GitLab instance under the <a href="https://git.ryujinx.app/ryubing/ryujinx/-/blob/master/LICENSE.txt?ref_type=heads" target="_blank">MIT license</a>.
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>.
<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/groups/ryubing/-/wikis/home">Wiki tab</a>.
Guides and documentation can be found on the <a href="https://git.ryujinx.app/projects/Ryubing/wiki/Home">Wiki tab</a>.
</p>
<p align="center">
<img src="https://git.ryujinx.app/ryubing/ryujinx/-/raw/master/docs/shell.png?ref_type=heads&inline=false" alt="Ryujinx example">
<img src="https://git.ryujinx.app/projects/Ryubing/raw/branch/master/docs/shell.png" 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/ryubing/ryujinx/-/releases).
You can find the stable releases [here](https://git.ryujinx.app/projects/Ryubing/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](docs/README.md).
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).
## 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](LICENSE.txt).
This software is licensed under the terms of the [MIT license](https://git.ryujinx.app/projects/Ryubing/src/branch/master/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](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY.md) for more details.
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.
## Credits
- [LibHac](https://git.ryujinx.app/ryubing/libhac) is used for our file-system.
- [LibHac](https://git.ryujinx.app/projects/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
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\canary.yml = .github\workflows\canary.yml
.forgejo\workflows\build.yml = .forgejo\workflows\build.yml
.forgejo\workflows\canary.yml = .forgejo\workflows\canary.yml
Directory.Packages.props = Directory.Packages.props
Directory.Build.props = Directory.Build.props
.github\workflows\release.yml = .github\workflows\release.yml
.forgejo\workflows\release.yml = .forgejo\workflows\release.yml
nuget.config = nuget.config
EndProjectSection
EndProject
@@ -573,6 +573,16 @@ 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": ""
"zh_TW": "啟動 RenderDoc 畫格擷取"
}
},
{
@@ -47,7 +47,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "结束 RenderDoc 帧捕获",
"zh_TW": ""
"zh_TW": "停止 RenderDoc 畫格擷取"
}
},
{
@@ -72,7 +72,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "丢弃 RenderDoc 帧捕获",
"zh_TW": ""
"zh_TW": "捨棄 RenderDoc 畫格擷取"
}
},
{
@@ -97,7 +97,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。",
"zh_TW": ""
"zh_TW": "停止正在執行的 RenderDoc 畫格擷取,且立即捨棄其結果。"
}
}
]

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 -n --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 21 \
AppDir "$OUTDIR"/Ryujinx.AppImage
appimagetool --appimage-extract-and-run -n --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 21 \
AppDir "$OUTDIR"/Ryujinx.AppImage

View File

@@ -0,0 +1,120 @@
#!/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,8 +5,7 @@
<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/v4/projects/17/packages/nuget/index.json" />
<add key="Ryujinx.UpdateClient" value="https://git.ryujinx.app/api/v4/projects/71/packages/nuget/index.json" />
<add key="LibHacAlpha" value="https://git.ryujinx.app/api/packages/projects/nuget/index.json" />
</packageSources>
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
@@ -14,10 +13,6 @@
<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<PhysicalKey> { }
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<Key> { }
}

View File

@@ -1,142 +0,0 @@
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,15 +16,6 @@ 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)
@@ -53,10 +44,6 @@ 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/ryubing/ryujinx/-/compare/Canary-{currentVersion}...Canary-{newVersion}"
: $"https://git.ryujinx.app/ryubing/ryujinx/-/releases/{newVersion}";
? $"https://git.ryujinx.app/projects/Ryubing/compare/Canary-{currentVersion}...Canary-{newVersion}"
: $"https://git.ryujinx.app/projects/Ryubing/releases/tag/{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/ryubing/ryujinx/-/wikis/FAQ-&-Troubleshooting";
public const string FaqWikiUrl = "https://git.ryujinx.app/projects/Ryubing/wiki/FAQ-%26-Troubleshooting";
public const string SetupGuideWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
"https://git.ryujinx.app/projects/Ryubing/wiki/Setup-%26-Configuration-Guide";
public const string MultiplayerWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
"https://git.ryujinx.app/projects/Ryubing/wiki/Multiplayer-(LDN-Local-Wireless)-Guide";
}
}

View File

@@ -118,8 +118,11 @@ 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));
Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), screenshotData.Length);
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 SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
using FileStream file = File.OpenWrite(filePath);
data.SaveTo(file);

View File

@@ -151,7 +151,9 @@ namespace Ryujinx.Input.SDL3
result |= GamepadFeaturesFlag.Led;
}
SDL_UnlockProperties(propID);
SDL_DestroyProperties(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).
return result;
}

View File

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

View File

@@ -8,15 +8,25 @@ using System.Runtime.CompilerServices;
using System.Threading;
using SDL;
using static SDL.SDL3;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
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<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private static readonly SDL_Keycode[] _keysDriverMapping =
@@ -161,8 +171,9 @@ namespace Ryujinx.Input.SDL3
SDL_Keycode.SDLK_0
];
public SDL3Keyboard(string id, string name)
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
{
_driver = driver;
Id = id;
Name = name;
_buttonsUserMapping = [];
@@ -184,9 +195,9 @@ namespace Ryujinx.Input.SDL3
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe static int ToSDL3Scancode(ConfigPhysicalKey key)
private unsafe static int ToSDL3Scancode(Key key)
{
if (key is >= ConfigPhysicalKey.Unknown and <= ConfigPhysicalKey.Menu)
if (key is >= Key.Unknown and <= Key.Menu)
{
return -1;
}
@@ -194,18 +205,18 @@ namespace Ryujinx.Input.SDL3
return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null);
}
private static SDL_Keymod GetKeyboardModifierMask(ConfigPhysicalKey key)
private static SDL_Keymod GetKeyboardModifierMask(Key key)
{
return key switch
{
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,
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,
// NOTE: Menu key isn't supported by SDL3.
_ => SDL_Keymod.SDL_KMOD_NONE
};
@@ -221,9 +232,9 @@ namespace Ryujinx.Input.SDL3
rawKeyboardState = SDL_GetKeyboardState(null);
}
bool[] keysState = new bool[(int)ConfigPhysicalKey.Count];
bool[] keysState = new bool[(int)Key.Count];
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
for (Key key = 0; key < Key.Count; key++)
{
int index = ToSDL3Scancode(key);
if (index == -1)
@@ -253,6 +264,36 @@ 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();
@@ -265,9 +306,9 @@ namespace Ryujinx.Input.SDL3
return result;
}
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (!entry.IsValid)
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
{
continue;
}
@@ -279,8 +320,8 @@ namespace Ryujinx.Input.SDL3
}
}
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -316,15 +357,38 @@ namespace Ryujinx.Input.SDL3
{
_configuration = (StandardKeyboardInputConfig)configuration;
// First clear the buttons mapping
_buttonsUserMapping.Clear();
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
// 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));
}
}
public void SetLed(uint packedRgb)
{
Logger.Debug?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
Logger.Info?.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(_keyboardIdentifers[0], "All keyboards");
return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
}
public IEnumerable<IGamepad> GetGamepads()

View File

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

View File

@@ -2,7 +2,6 @@ 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;
@@ -235,9 +234,7 @@ namespace Ryujinx.Input.HLE
_gamepad?.Dispose();
Id = config.Id;
_gamepad = config is StandardKeyboardInputConfig && GamepadDriver is IKeyboardModeDriver keyboardModeDriver
? keyboardModeDriver.GetKeyboard(Id, KeyboardInputMode.Physical)
: GamepadDriver.GetGamepad(Id);
_gamepad = GamepadDriver.GetGamepad(Id);
UpdateUserConfiguration(config);
@@ -566,7 +563,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,7 +36,6 @@ namespace Ryujinx.Input.HLE
private bool _isDisposed;
private List<InputConfig> _inputConfig;
private List<InputConfig> _requestedInputConfig;
private bool _enableKeyboard;
private bool _enableMouse;
private Switch _device;
@@ -53,7 +52,6 @@ namespace Ryujinx.Input.HLE
_gamepadDriver = gamepadDriver;
_mouseDriver = mouseDriver;
_inputConfig = [];
_requestedInputConfig = [];
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -91,14 +89,14 @@ namespace Ryujinx.Input.HLE
}
}
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
}
}
private void HandleOnGamepadConnected(string id)
{
// Force input reload
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -129,13 +127,11 @@ namespace Ryujinx.Input.HLE
{
lock (_lock)
{
_requestedInputConfig = inputConfig?.ToList() ?? [];
NpadController[] oldControllers = _controllers.ToArray();
List<InputConfig> validInputs = [];
foreach (InputConfig inputConfigEntry in _requestedInputConfig)
foreach (InputConfig inputConfigEntry in inputConfig)
{
NpadController controller;
int index = (int)inputConfigEntry.PlayerIndex;
@@ -151,17 +147,7 @@ namespace Ryujinx.Input.HLE
controller = new(_cemuHookClient);
}
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);
}
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
if (!isValid)
{
@@ -171,7 +157,7 @@ namespace Ryujinx.Input.HLE
else
{
_controllers[index] = controller;
validInputs.Add(activeConfig);
validInputs.Add(inputConfigEntry);
}
}
@@ -183,7 +169,7 @@ namespace Ryujinx.Input.HLE
oldControllers[i] = null;
}
_inputConfig = validInputs;
_inputConfig = inputConfig;
_enableKeyboard = enableKeyboard;
_enableMouse = enableMouse;
@@ -191,79 +177,6 @@ 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)
@@ -421,7 +334,7 @@ namespace Ryujinx.Input.HLE
}
}
public InputConfig GetPlayerInputConfigByIndex(int index)
internal InputConfig GetPlayerInputConfigByIndex(int index)
{
lock (_lock)
{

View File

@@ -1,6 +1,5 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input
{
@@ -34,26 +33,15 @@ namespace Ryujinx.Input
{
if (_keyState is null)
{
_keyState = new bool[(int)ConfigPhysicalKey.Count];
_keyState = new bool[(int)Key.Count];
}
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
for (Key key = 0; key < Key.Count; key++)
{
_keyState[(int)key] = keyboard.IsPressed((Key)(int)key);
_keyState[(int)key] = keyboard.IsPressed(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

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

View File

@@ -1,78 +0,0 @@
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

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

View File

@@ -1,5 +1,4 @@
using System.Runtime.CompilerServices;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input
{
@@ -26,8 +25,5 @@ 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="ppy.SDL3-CS" />
<PackageReference Include="Ryujinx.SDL3-CS" />
</ItemGroup>
</Project>

View File

@@ -61,6 +61,14 @@ 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(0x24, Unsafe.SizeOf<BiquadFilterEffectParameter2>());
Assert.AreEqual(0x28, Unsafe.SizeOf<BiquadFilterEffectParameter2>());
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

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 PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Headless
{
@@ -105,48 +105,48 @@ namespace Ryujinx.Headless
Backend = InputBackendType.WindowKeyboard,
Id = null,
ControllerType = ControllerType.JoyconPair,
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
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,
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,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
},
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
RightJoycon = new RightJoyconCommonConfig<Key>
{
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,
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,
},
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.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<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly AvaloniaKeyboardDriver _driver;
private readonly KeyboardInputMode _mode;
private StandardKeyboardInputConfig _configuration;
private readonly Lock _userMappingLock = new();
@@ -24,12 +24,18 @@ namespace Ryujinx.Ava.Input
public bool IsConnected => true;
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name, KeyboardInputMode mode)
private class ButtonMappingEntry(GamepadButtonInputId to, Key from)
{
public readonly GamepadButtonInputId To = to;
public readonly Key From = from;
}
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name)
{
_buttonsUserMapping = [];
_driver = driver;
_mode = mode;
Id = id;
Name = name;
}
@@ -51,18 +57,22 @@ namespace Ryujinx.Ava.Input
return result;
}
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (!entry.IsValid || result.IsPressed(entry.To))
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
{
continue;
}
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
// NOTE: Do not touch state of the button already pressed.
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -90,7 +100,7 @@ namespace Ryujinx.Ava.Input
{
try
{
return _driver.IsPressed(key, _mode);
return _driver.IsPressed(key);
}
catch
{
@@ -98,19 +108,6 @@ 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)
@@ -119,13 +116,37 @@ namespace Ryujinx.Ava.Input
_buttonsUserMapping.Clear();
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
#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
}
}
public void SetLed(uint packedRgb)
{
Logger.Debug?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
}
public void SetTriggerThreshold(float triggerThreshold) { }
@@ -141,9 +162,41 @@ 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(_mode);
_driver?.Clear();
}
public void Dispose() { }

View File

@@ -1,35 +1,19 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
using System.Threading;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using AvaKey = Avalonia.Input.Key;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.Input
{
internal class AvaloniaKeyboardDriver : IKeyboardModeDriver
internal class AvaloniaKeyboardDriver : IGamepadDriver
{
private enum PhysicalKeySource
{
Direct,
ObservedFallback,
Unknown,
}
private static readonly string[] _keyboardIdentifers = ["0"];
private readonly Control _control;
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;
private readonly HashSet<AvaKey> _pressedKeys;
public event EventHandler<KeyEventArgs> KeyPressed;
public event EventHandler<KeyEventArgs> KeyRelease;
@@ -38,22 +22,13 @@ namespace Ryujinx.Ava.Input
public string DriverName => "AvaloniaKeyboardDriver";
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
public AvaloniaKeyboardDriver(Control control, KeyboardInputMode defaultMode = KeyboardInputMode.Semantic)
public AvaloniaKeyboardDriver(Control control)
{
_control = control;
_semanticPressedKeys = [];
_physicalPressedKeys = [];
_observedPhysicalKeysBySemanticKey = [];
_semanticPressedKeyQueue = [];
_physicalPressedKeyQueue = [];
_pressedKeyQueueLock = new();
_defaultMode = defaultMode;
_pressedKeys = [];
// 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.KeyDown += OnKeyPress;
_control.KeyUp += OnKeyRelease;
_control.TextInput += Control_TextInput;
}
@@ -75,18 +50,13 @@ 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.KeyboardLayout_KeyboardInputMode], mode);
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]);
}
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
@@ -95,179 +65,40 @@ namespace Ryujinx.Ava.Input
{
if (disposing)
{
_control.RemoveHandler(InputElement.KeyDownEvent, OnKeyPress);
_control.RemoveHandler(InputElement.KeyUpEvent, OnKeyRelease);
_control.TextInput -= Control_TextInput;
_control.KeyUp -= OnKeyPress;
_control.KeyDown -= OnKeyRelease;
}
}
protected void OnKeyPress(object sender, KeyEventArgs args)
{
UpdateKeyStates(args, isPressed: true);
_pressedKeys.Add(args.Key);
KeyPressed?.Invoke(this, args);
}
protected void OnKeyRelease(object sender, KeyEventArgs args)
{
UpdateKeyStates(args, isPressed: false);
_pressedKeys.Remove(args.Key);
KeyRelease?.Invoke(this, args);
}
internal bool IsPressed(Key key, KeyboardInputMode mode)
internal bool IsPressed(Key key)
{
if (key is Key.Unbound or Key.Unknown)
{
return false;
}
return mode == KeyboardInputMode.Physical
? _physicalPressedKeys.Contains((ConfigPhysicalKey)(int)key)
: _semanticPressedKeys.Contains(key);
}
AvaloniaKeyboardMappingHelper.TryGetAvaKey(key, out AvaKey nativeKey);
internal void Clear(KeyboardInputMode mode)
{
lock (_pressedKeyQueueLock)
{
if (mode == KeyboardInputMode.Physical)
{
_physicalPressedKeys.Clear();
_physicalPressedKeyQueue.Clear();
}
else
{
_semanticPressedKeys.Clear();
_semanticPressedKeyQueue.Clear();
}
}
return _pressedKeys.Contains(nativeKey);
}
public void 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 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;
}
Logger.Trace?.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;
_pressedKeys.Clear();
}
public void Dispose()

View File

@@ -2,7 +2,6 @@ using Ryujinx.Input;
using System;
using System.Collections.Generic;
using AvaKey = Avalonia.Input.Key;
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
namespace Ryujinx.Ava.Input
{
@@ -133,8 +132,7 @@ namespace Ryujinx.Ava.Input
AvaKey.D8,
AvaKey.D9,
AvaKey.OemTilde,
AvaKey.Oem102,
AvaKey.OemMinus,
AvaKey.OemTilde,AvaKey.OemMinus,
AvaKey.OemPlus,
AvaKey.OemOpenBrackets,
AvaKey.OemCloseBrackets,
@@ -149,149 +147,7 @@ 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()
{
@@ -299,42 +155,21 @@ 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 avaKey))
if (TryGetAvaKey(key, out AvaKey index))
{
_avaKeyMapping[avaKey] = key;
}
if (TryGetAvaPhysicalKey(key, out AvaPhysicalKey avaPhysicalKey))
{
_avaPhysicalKeyMapping[avaPhysicalKey] = key;
_avaKeyMapping[index] = 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 = key < Key.Count && (int)key < _keyMapping.Length;
bool keyExist = (int)key < _keyMapping.Length;
if (keyExist)
{
avaKey = _keyMapping[(int)key];
@@ -343,34 +178,9 @@ 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,9 +24,11 @@ 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
@@ -52,6 +54,22 @@ 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,6 +46,7 @@
<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" />
@@ -68,7 +69,6 @@
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
<PackageReference Include="SPB" />
<PackageReference Include="SharpZipLib" />
</ItemGroup>
<ItemGroup>
@@ -169,9 +169,8 @@
<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

@@ -13,8 +13,6 @@ 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
@@ -271,45 +269,45 @@ namespace Ryujinx.Ava.Systems.Configuration
Id = "0",
PlayerIndex = PlayerIndex.Player1,
ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
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,
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,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
},
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
RightJoycon = new RightJoyconCommonConfig<Key>
{
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,
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,
},
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
},
}
];

View File

@@ -8,8 +8,6 @@ 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
{
@@ -287,45 +285,45 @@ namespace Ryujinx.Ava.Systems.Configuration
Name = "Keyboard",
PlayerIndex = PlayerIndex.Player1,
ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
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,
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,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
},
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
RightJoycon = new RightJoyconCommonConfig<Key>
{
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,
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,
},
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
},
}
];

View File

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

View File

@@ -1,19 +1,20 @@
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;
@@ -21,7 +22,6 @@ 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,27 +73,6 @@ 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
@@ -143,6 +122,14 @@ 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()
@@ -153,6 +140,27 @@ 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) =>
{
@@ -234,22 +242,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.
@@ -258,7 +266,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}");
}
@@ -275,7 +283,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) =>
@@ -294,10 +302,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;
@@ -402,73 +410,33 @@ namespace Ryujinx.Ava.Systems
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private static void ExtractTarGzipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
private static void ExtractTarGzipFile(string archivePath, string outputDirectoryPath)
{
using FileStream inStream = File.OpenRead(archivePath);
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);
});
}
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);
}
private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
private static void ExtractZipFile(string archivePath, string 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);
});
}
ZipFile.ExtractToDirectory(archivePath, outputDirectoryPath);
}
private static void Extract7ZipFile(string archivePath, string outputDirectoryPath)
{
IArchive archive = ArchiveFactory.OpenArchive(archivePath);
archive.WriteToDirectory(outputDirectoryPath);
}
private static void InstallUpdate(TaskDialog taskDialog, string updateFile)
@@ -479,16 +447,20 @@ namespace Ryujinx.Ava.Systems
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
ExtractTarXzipFile(updateFile, _updateDir);
}
else if (OperatingSystem.IsWindows())
{
ExtractZipFile(taskDialog, updateFile, _updateDir);
Extract7ZipFile(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.PhysicalKey, e.Key);
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(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.PhysicalKey, e.Key);
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
{

View File

@@ -1,6 +1,5 @@
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using Ryujinx.Ava.Input;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System;
@@ -26,7 +25,6 @@ namespace Ryujinx.Ava.UI.Helpers
private bool _isWaitingForInput;
private bool _shouldUnbind;
private IKeyboard _keyboard;
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
public ButtonKeyAssigner(ToggleButton toggleButton)
@@ -36,9 +34,6 @@ namespace Ryujinx.Ava.UI.Helpers
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
{
_keyboard = keyboard;
ClearKeyboardState(_keyboard);
Dispatcher.UIThread.Post(() =>
{
ToggledButton.IsChecked = true;
@@ -87,7 +82,6 @@ namespace Ryujinx.Ava.UI.Helpers
_isWaitingForInput = false;
ToggledButton.IsChecked = false;
ClearKeyboardState(_keyboard);
if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace)
{
@@ -104,15 +98,6 @@ 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,7 +5,6 @@ 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
{
@@ -13,6 +12,79 @@ 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 },
@@ -38,38 +110,49 @@ namespace Ryujinx.Ava.UI.Helpers
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
{ GamepadInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
{ GamepadInputId.Unbound, LocaleKeys.KeyUnbound},
};
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
{
{ StickInputId.Left, LocaleKeys.StickLeft},
{ StickInputId.Right, LocaleKeys.StickRight},
{ StickInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
{ StickInputId.Unbound, LocaleKeys.KeyUnbound},
};
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string keyString = string.Empty;
LocaleKeys localeKey;
switch (value)
{
case Key key:
if (KeyboardLayoutLocaleHelper.TryGetSemanticLabel(key, out string localizedKeyLabel))
if (_keysMap.TryGetValue(key, out localeKey))
{
keyString = 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];
}
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

@@ -1,28 +0,0 @@
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

@@ -1,142 +0,0 @@
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

@@ -1,226 +0,0 @@
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 PhysicalKey LeftStickUp { get; set; }
public partial Key LeftStickUp { get; set; }
[ObservableProperty]
public partial PhysicalKey LeftStickDown { get; set; }
public partial Key LeftStickDown { get; set; }
[ObservableProperty]
public partial PhysicalKey LeftStickLeft { get; set; }
public partial Key LeftStickLeft { get; set; }
[ObservableProperty]
public partial PhysicalKey LeftStickRight { get; set; }
public partial Key LeftStickRight { get; set; }
[ObservableProperty]
public partial PhysicalKey LeftStickButton { get; set; }
public partial Key LeftStickButton { get; set; }
[ObservableProperty]
public partial PhysicalKey RightStickUp { get; set; }
public partial Key RightStickUp { get; set; }
[ObservableProperty]
public partial PhysicalKey RightStickDown { get; set; }
public partial Key RightStickDown { get; set; }
[ObservableProperty]
public partial PhysicalKey RightStickLeft { get; set; }
public partial Key RightStickLeft { get; set; }
[ObservableProperty]
public partial PhysicalKey RightStickRight { get; set; }
public partial Key RightStickRight { get; set; }
[ObservableProperty]
public partial PhysicalKey RightStickButton { get; set; }
public partial Key RightStickButton { get; set; }
[ObservableProperty]
public partial PhysicalKey DpadUp { get; set; }
public partial Key DpadUp { get; set; }
[ObservableProperty]
public partial PhysicalKey DpadDown { get; set; }
public partial Key DpadDown { get; set; }
[ObservableProperty]
public partial PhysicalKey DpadLeft { get; set; }
public partial Key DpadLeft { get; set; }
[ObservableProperty]
public partial PhysicalKey DpadRight { get; set; }
public partial Key DpadRight { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonMinus { get; set; }
public partial Key ButtonMinus { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonPlus { get; set; }
public partial Key ButtonPlus { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonA { get; set; }
public partial Key ButtonA { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonB { get; set; }
public partial Key ButtonB { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonX { get; set; }
public partial Key ButtonX { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonY { get; set; }
public partial Key ButtonY { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonL { get; set; }
public partial Key ButtonL { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonR { get; set; }
public partial Key ButtonR { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonZl { get; set; }
public partial Key ButtonZl { get; set; }
[ObservableProperty]
public partial PhysicalKey ButtonZr { get; set; }
public partial Key ButtonZr { get; set; }
[ObservableProperty]
public partial PhysicalKey LeftButtonSl { get; set; }
public partial Key LeftButtonSl { get; set; }
[ObservableProperty]
public partial PhysicalKey LeftButtonSr { get; set; }
public partial Key LeftButtonSr { get; set; }
[ObservableProperty]
public partial PhysicalKey RightButtonSl { get; set; }
public partial Key RightButtonSl { get; set; }
[ObservableProperty]
public partial PhysicalKey RightButtonSr { get; set; }
public partial Key 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<PhysicalKey>
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
DpadUp = DpadUp,
DpadDown = DpadDown,
@@ -165,7 +165,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ButtonSl = LeftButtonSl,
ButtonSr = LeftButtonSr,
},
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
RightJoycon = new RightJoyconCommonConfig<Key>
{
ButtonA = ButtonA,
ButtonB = ButtonB,
@@ -177,7 +177,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ButtonR = ButtonR,
ButtonZr = ButtonZr,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = LeftStickUp,
StickDown = LeftStickDown,
@@ -185,7 +185,7 @@ namespace Ryujinx.Ava.UI.Models.Input
StickLeft = LeftStickLeft,
StickButton = LeftStickButton,
},
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = RightStickUp,
StickDown = RightStickDown,
@@ -198,37 +198,5 @@ 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(KeyboardConfig.LeftStickRight))
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
{
leftBuffer.Item1 += 1;
}
if (snapshot.IsPressed(KeyboardConfig.LeftStickLeft))
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
{
leftBuffer.Item1 -= 1;
}
if (snapshot.IsPressed(KeyboardConfig.LeftStickUp))
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
{
leftBuffer.Item2 += 1;
}
if (snapshot.IsPressed(KeyboardConfig.LeftStickDown))
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
{
leftBuffer.Item2 -= 1;
}
if (snapshot.IsPressed(KeyboardConfig.RightStickRight))
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
{
rightBuffer.Item1 += 1;
}
if (snapshot.IsPressed(KeyboardConfig.RightStickLeft))
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
{
rightBuffer.Item1 -= 1;
}
if (snapshot.IsPressed(KeyboardConfig.RightStickUp))
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
{
rightBuffer.Item2 += 1;
}
if (snapshot.IsPressed(KeyboardConfig.RightStickDown))
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
{
rightBuffer.Item2 -= 1;
}

View File

@@ -45,6 +45,7 @@ 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 GitLabLogo { get; set; }
[ObservableProperty] public partial Bitmap ForgejoLogo { get; set; }
[ObservableProperty] public partial Bitmap DiscordLogo { get; set; }
@@ -37,6 +37,7 @@ 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)
{
@@ -46,7 +47,7 @@ namespace Ryujinx.Ava.UI.ViewModels
string themeName = isDarkTheme ? "Dark" : "Light";
DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
GitLabLogo = LoadBitmap(LogoPathFormat.Format("GitLab", themeName));
ForgejoLogo = LoadBitmap(UnthemedLogoPathFormat.Format("Forgejo"));
}
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
@@ -55,7 +56,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
GitLabLogo.Dispose();
ForgejoLogo.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.RefreshModifiedState();
ParentModel.IsModified = true;
}
public async void ShowRumbleConfig()
{
await RumbleInputView.Show(this);
ParentModel.RefreshModifiedState();
ParentModel.IsModified = true;
}
public async void ShowLedConfig()
{
await LedInputView.Show(this);
ParentModel.RefreshModifiedState();
ParentModel.IsModified = true;
}
public void OnParentModelChanged()

View File

@@ -1,7 +1,6 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Svg.Skia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using Ryujinx.Ava.Common.Locale;
@@ -29,7 +28,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 PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
@@ -43,7 +42,6 @@ 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;
@@ -67,7 +65,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public IGamepadDriver AvaloniaKeyboardDriver { get; private set; }
public IGamepadDriver AvaloniaKeyboardDriver { get; }
public IGamepad SelectedGamepad
{
@@ -91,6 +89,7 @@ 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;
@@ -100,6 +99,7 @@ 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,6 +163,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
LoadDevice();
LoadProfiles();
RevertDeviceId = Devices[Device].Id;
_isLoaded = true;
_isChangeTrackingActive = true;
OnPropertyChanged();
@@ -174,56 +175,50 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
get => _controller;
set
{
int controllerIndex = value < 0 ? 0 : value;
MarkAsChanged();
if (controllerIndex == _controller)
_controller = value;
if (_controller == -1)
{
return;
_controller = 0;
}
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)
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
{
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;
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();
}
LoadInputDriver();
LoadProfiles();
OnPropertyChanged();
NotifyChanges();
}
OnPropertyChanged(nameof(Controller));
NotifyChanges();
}
public string ControllerImage
@@ -260,83 +255,33 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
get => _device;
set
{
if (value < 0 || value >= Devices.Count)
MarkAsChanged();
_device = value < 0 ? 0 : value;
if (_device >= Devices.Count)
{
return;
}
_device = value;
DeviceType selected = Devices[_device].Type;
if (selected != DeviceType.None)
{
LoadControllers();
if (_isLoaded)
{
LoadSelectedDeviceDefaults();
}
else
{
LoadSelectedDeviceControllers();
LoadConfiguration(LoadDefaultConfiguration());
}
}
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()
@@ -345,8 +290,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_mainWindow = RyujinxApp.MainWindow;
ReplaceKeyboardDriver(owner);
PhysicalKeyLabelHelper.LabelsChanged += OnPhysicalKeyLabelsChanged;
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner);
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -357,7 +301,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_isLoaded = false;
RefreshAvailableDevices();
LoadDevices();
PlayerId = PlayerIndex.Player1;
}
@@ -365,22 +309,13 @@ 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;
@@ -398,19 +333,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private InputConfig GetPersistedInputConfig()
private void LoadConfiguration(InputConfig inputConfig = null)
{
if (UseGlobalConfig && Program.UseExtraConfig)
{
return ConfigurationState.InstanceExtra.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
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.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
}
private void LoadConfiguration(InputConfig inputConfig = null)
{
Config = inputConfig ?? GetDisplayedInputConfig(GetPersistedInputConfig());
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
{
@@ -423,18 +355,6 @@ 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
@@ -455,6 +375,16 @@ 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
@@ -465,117 +395,34 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public void LoadDevice()
{
int deviceIndex = 0;
if (Config == null || Config.Backend == InputBackendType.Invalid)
{
ApplyLoadedDevice(deviceIndex);
return;
Device = 0;
}
DeviceType type = DeviceType.None;
if (Config is StandardKeyboardInputConfig)
else
{
type = DeviceType.Keyboard;
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;
}
}
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()
@@ -615,24 +462,11 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_isChangeTrackingActive = false; // Disable configuration change tracking
bool shouldApplyKeyboardFallback = Config is StandardControllerInputConfig controllerConfig && controllerConfig.Id == id;
LoadDevices();
RefreshAvailableDevices();
if (shouldApplyKeyboardFallback)
{
LoadConfiguration();
LoadDevice();
NotificationIsVisible = false;
IsModified = false;
NotifyChanges();
}
else
{
IsModified = true;
RevertChanges();
FindPairedDeviceInConfigFile();
}
IsModified = true;
RevertChanges();
FindPairedDeviceInConfigFile();
_isChangeTrackingActive = true; // Enable configuration change tracking
@@ -642,7 +476,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_isChangeTrackingActive = false; // Disable configuration change tracking
RefreshAvailableDevices();
LoadDevices();
IsModified = true;
RevertChanges();
@@ -668,23 +502,6 @@ 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();
@@ -693,7 +510,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeHandheld]));
ApplyControllerSelection(0);
Controller = 0;
}
else
{
@@ -711,14 +528,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
// Workaround: set the box to 1 and then 0
if (controllerIndex == 0)
{
ApplyControllerSelection(1);
Controller = 1;
}
ApplyControllerSelection(controllerIndex);
Controller = controllerIndex;
}
else
{
ApplyControllerSelection(0);
Controller = 0;
}
}
}
@@ -744,16 +561,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
return str[(str.IndexOf(Hyphen) + Offset)..];
}
private void RefreshAvailableDevices()
public void LoadDevices()
{
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})";
@@ -774,6 +583,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
lock (Devices)
{
Devices.Clear();
DeviceList.Clear();
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
@@ -799,20 +609,9 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
}
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);
}
DeviceList.AddRange(Devices.Select(x => x.Name));
Device = Math.Min(Device, DeviceList.Count);
}
ApplyLoadedDevice(selectedDeviceIndex);
}
private string GetProfileBasePath()
@@ -878,46 +677,46 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
Id = id,
Name = name,
ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
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,
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,
},
LeftJoyconStick =
new JoyconConfigKeyboardStick<PhysicalKey>
new JoyconConfigKeyboardStick<Key>
{
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
},
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
RightJoycon = new RightJoyconCommonConfig<Key>
{
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,
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,
},
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
},
};
}
@@ -1061,14 +860,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_isLoaded = false;
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)
config.Id = Config.Id; // Set current device id instead of changing device(independent profiles)
LoadConfiguration(config);
@@ -1166,6 +958,9 @@ 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();
@@ -1185,6 +980,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
IsModified = false;
RevertDeviceId = Devices[Device].Id; // Remember selected device after saving
List<InputConfig> newConfig = [];
if (UseGlobalConfig && Program.UseExtraConfig)
@@ -1204,7 +1001,25 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
else
{
InputConfig config = GetSelectedDeviceConfig();
(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;
int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId);
if (i == -1)
@@ -1245,46 +1060,9 @@ 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;
@@ -1295,7 +1073,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 AboutGitLabUrlTooltipMessage}">
<Image Source="{Binding GitLabLogo}" />
ToolTip.Tip="{ext:Locale AboutForgejoUrlTooltipMessage}">
<Image Source="{Binding ForgejoLogo}" />
</Button>
<Button
MinWidth="30"

View File

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

View File

@@ -3,7 +3,6 @@
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"
@@ -149,7 +148,7 @@
<Grid
Grid.Column="0"
Margin="2"
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto,Auto">
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto">
<TextBlock
Grid.Column="0"
Margin="5,0,10,0"
@@ -162,38 +161,19 @@
Name="DeviceBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ItemsSource="{Binding Devices}"
SelectedItem="{Binding SelectedDeviceItem, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ., Converter={x:Static helpers:InputDeviceNameConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
ItemsSource="{Binding DeviceList}"
SelectedIndex="{Binding Device}" />
<Button
Grid.Column="2"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale ControllerSettingsRefresh}"
Command="{Binding RefreshInputDevices}">
Command="{Binding LoadDevice}">
<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,5 +1,4 @@
using Avalonia.Controls;
using Avalonia;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
@@ -16,14 +15,9 @@ namespace Ryujinx.Ava.UI.Views.Input
public InputView()
{
ReplaceViewModel(ConfigurationState.Instance.System.UseInputGlobalConfig);
}
ViewModel = new InputViewModel(this, ConfigurationState.Instance.System.UseInputGlobalConfig);
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
ViewModel?.RetargetKeyboardDriver(this);
InitializeComponent();
}
public void SaveCurrentProfile()
@@ -34,18 +28,8 @@ namespace Ryujinx.Ava.UI.Views.Input
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
{
Dispose();
ReplaceViewModel(enableConfigGlobal);
}
private void ReplaceViewModel(bool useGlobalConfig)
{
ViewModel = new InputViewModel(this, useGlobalConfig); // Create new Input Page with the selected input config scope.
ViewModel = new InputViewModel(this, enableConfigGlobal); // Create new Input Page with global input configs
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 PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Ava.UI.Views.Input
{
@@ -73,96 +73,95 @@ 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<PhysicalKey>();
ViewModel.Config.ButtonZl = buttonValue.AsHidType<Key>();
break;
case "ButtonL":
ViewModel.Config.ButtonL = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.ButtonL = buttonValue.AsHidType<Key>();
break;
case "ButtonMinus":
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<Key>();
break;
case "LeftStickButton":
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<Key>();
break;
case "LeftStickUp":
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<Key>();
break;
case "LeftStickDown":
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<Key>();
break;
case "LeftStickRight":
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<Key>();
break;
case "LeftStickLeft":
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<Key>();
break;
case "DpadUp":
ViewModel.Config.DpadUp = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.DpadUp = buttonValue.AsHidType<Key>();
break;
case "DpadDown":
ViewModel.Config.DpadDown = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.DpadDown = buttonValue.AsHidType<Key>();
break;
case "DpadLeft":
ViewModel.Config.DpadLeft = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.DpadLeft = buttonValue.AsHidType<Key>();
break;
case "DpadRight":
ViewModel.Config.DpadRight = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.DpadRight = buttonValue.AsHidType<Key>();
break;
case "LeftButtonSr":
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<Key>();
break;
case "LeftButtonSl":
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<Key>();
break;
case "RightButtonSr":
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<Key>();
break;
case "RightButtonSl":
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<Key>();
break;
case "ButtonZr":
ViewModel.Config.ButtonZr = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.ButtonZr = buttonValue.AsHidType<Key>();
break;
case "ButtonR":
ViewModel.Config.ButtonR = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.ButtonR = buttonValue.AsHidType<Key>();
break;
case "ButtonPlus":
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<Key>();
break;
case "ButtonA":
ViewModel.Config.ButtonA = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.ButtonA = buttonValue.AsHidType<Key>();
break;
case "ButtonB":
ViewModel.Config.ButtonB = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.ButtonB = buttonValue.AsHidType<Key>();
break;
case "ButtonX":
ViewModel.Config.ButtonX = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.ButtonX = buttonValue.AsHidType<Key>();
break;
case "ButtonY":
ViewModel.Config.ButtonY = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.ButtonY = buttonValue.AsHidType<Key>();
break;
case "RightStickButton":
ViewModel.Config.RightStickButton = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.RightStickButton = buttonValue.AsHidType<Key>();
break;
case "RightStickUp":
ViewModel.Config.RightStickUp = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.RightStickUp = buttonValue.AsHidType<Key>();
break;
case "RightStickDown":
ViewModel.Config.RightStickDown = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.RightStickDown = buttonValue.AsHidType<Key>();
break;
case "RightStickRight":
ViewModel.Config.RightStickRight = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.RightStickRight = buttonValue.AsHidType<Key>();
break;
case "RightStickLeft":
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<PhysicalKey>();
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<Key>();
break;
}
ViewModel.ParentModel.RefreshModifiedState();
}
};
@@ -208,40 +207,40 @@ namespace Ryujinx.Ava.UI.Views.Input
{
Dictionary<string, Action> buttonActions = new()
{
{ "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 }
{ "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 }
};
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
{
action();
ViewModel.ParentModel.RefreshModifiedState();
ViewModel.ParentModel.IsModified = true;
}
}
}

View File

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

View File

@@ -34,8 +34,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
}
}
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this, KeyboardInputMode.Semantic);
_avaloniaKeyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)

View File

@@ -30,7 +30,6 @@ 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;
@@ -106,9 +105,7 @@ namespace Ryujinx.Ava.UI.Windows
if (Program.PreviewerDetached)
{
AvaloniaKeyboardDriver keyboardDriver = new(this, KeyboardInputMode.Semantic);
keyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
InputManager = new InputManager(keyboardDriver, new SDL3GamepadDriver());
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL3GamepadDriver());
_ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it);
this.ScalingChanged += OnScalingChanged;