Compare commits

...

78 Commits

Author SHA1 Message Date
0e9acdfed0 Merge branch 'input-cycle' into 'master'
Input cycling and overlay system

See merge request [ryubing/ryujinx!72](https://git.ryujinx.app/ryubing/ryujinx/-/merge_requests/72)
2025-06-25 12:25:02 -05:00
154f056422 ControllerOverlay: truncate names from middle instead of end, (due to index identifier usually being on end) 2025-06-25 20:24:37 +03:00
58c2dc4ac4 Overlays: FlipY fix 2025-06-25 20:23:43 +03:00
Neo
0cc94fdf37 Update French Translation (ryubing/ryujinx!67)
See merge request ryubing/ryujinx!67
2025-06-23 14:50:47 -05:00
d6232008d5 Overlays: Move the structure to Ryujinx/UI/Overlay (also no longer need to cross-pass locales) 2025-06-22 03:34:46 -05:00
076dd9a56a Overlays: fix delta calculation 2025-06-22 03:34:46 -05:00
952be47f3c ControllerOverlay: don't need this parameter to be optional 2025-06-22 03:34:46 -05:00
b3c2fbcfa7 Flip overlays in Linux (yes hacky but works) 2025-06-22 03:34:46 -05:00
030d9f768f Replace System.Diagnostics.Debug.WriteLine with Ryujinx logger 2025-06-22 03:34:46 -05:00
d4f7dfabf4 Remove unused GetOverlayManager method from Window class 2025-06-22 03:34:46 -05:00
3ec079855d Extended hotkeys to player1-8 + h, localized the overlay 2025-06-22 03:34:46 -05:00
ef0dac5533 Removed bogus logs 2025-06-22 03:34:46 -05:00
10e6fbcb72 ControllerOverlay: remove auto generated translations 2025-06-22 03:34:46 -05:00
de25673820 ControllerOverlay: forgot to remove unused axaml one 2025-06-22 03:34:46 -05:00
ba250df73d Overlay system 2025-06-22 03:34:46 -05:00
bfd715b607 SettingsUIView: localize 2025-06-22 03:34:46 -05:00
d7929a7f0e Controller overlay changed from Window to UserControl 2025-06-22 03:34:46 -05:00
1e86aa9764 Controller overlay duration config 2025-06-22 03:34:46 -05:00
175d5f9bb3 Input cycling hotkeys 2025-06-22 03:34:46 -05:00
8765dc9901 Controller overlay showing which player is bound to which controller 2025-06-22 03:34:46 -05:00
74a9b94227 UI: Properly space total play time separator when loading bar is shown. 2025-06-20 23:06:16 -05:00
d3208a4c44 UI: Don't show total play time if there is none. 2025-06-20 23:02:39 -05:00
5d136980a3 fix: UI deadlock when launching a game with "Trace Logs" enabled (ryubing/ryujinx!70)
See merge request ryubing/ryujinx!70
2025-06-19 20:51:11 -05:00
572ad1eac5 Exclude time spent with emulator paused from play time (ryubing/ryujinx!55)
See merge request ryubing/ryujinx!55
2025-06-19 16:33:10 -05:00
6bb2af0091 Implement CreateLibraryAppletEx in ILibraryAppletCreator (ryubing/ryujinx!69)
See merge request ryubing/ryujinx!69
2025-06-19 15:48:06 -05:00
534a194ed9 Correct typo on part of the character for word "server" (ryubing/ryujinx!68)
See merge request ryubing/ryujinx!68
2025-06-19 15:25:40 -05:00
331805791e infra: [ci skip] fix inconsistent namespaces from update library 2025-06-19 04:26:22 -05:00
6773406bb6 infra: Use Ryujinx.UpdateClient NuGet package for checking for updates.
Main benefit to this is sharing the C# model definitions from what the server returns and Ryujinx uses in-app without differences.
Additionally removed the GitHub API JSON models.
2025-06-19 04:18:33 -05:00
6226eadf55 docs: compat: The Legend of Nayuta: Boundless Trails: ingame (ryubing/ryujinx!59) 2025-06-18 14:31:08 -05:00
b1cde5fd97 Updated Swedish translation (ryubing/ryujinx!66)
See merge request ryubing/ryujinx!66
2025-06-17 13:05:39 -05:00
39944b2063 Update Korean translation (ryubing/ryujinx!64)
See merge request ryubing/ryujinx!64
2025-06-17 03:21:30 -05:00
973c6ba5df UI: RPC: Squeakross: Home Squeak Home image
docs: compat: Squeakross: Home Squeak Home: Playable
2025-06-16 02:06:45 -05:00
6803c91da8 infra: Add package source mappings for Ryujinx.UpdateClient to silence compile warnings 2025-06-16 02:05:11 -05:00
557c2a50b2 infra: Add NuGet config to solution items 2025-06-16 02:04:48 -05:00
77a797f154 Revert "Structural and Memory Safety Improvements, Analyzer Cleanup (ryubing/ryujinx!47)"
This reverts merge request !47
2025-06-15 20:45:26 -05:00
faf9e3cdd7 macOS: Fix MoltenVK config packing (ryubing/ryujinx!65)
See merge request ryubing/ryujinx!65
2025-06-15 18:24:45 -05:00
7bc80ed4fe Updated Brazilian Portuguese translation (ryubing/ryujinx!62)
See merge request ryubing/ryujinx!62
2025-06-15 10:28:41 -05:00
a1d44ec496 Update translation for Traditional Chinese (ryubing/ryujinx!61)
See merge request ryubing/ryujinx!61
2025-06-14 20:06:12 -05:00
bab3beb0ac [ci skip] Forgot closing / lol 2025-06-13 15:51:23 -05:00
aa9e74339b Add support for notifying the update server when a new update has been pushed instead of relying on periodic refreshes 2025-06-13 01:57:54 -05:00
908273d848 [ci skip] UpdateClient package source
https://git.ryujinx.app/ryubing/update-server/-/packages
2025-06-13 01:57:54 -05:00
b51ad11574 Updated Simplified Chinese translation (ryubing/ryujinx!58)
See merge request ryubing/ryujinx!58
2025-06-11 19:43:50 -05:00
ea027d65a7 Structural and Memory Safety Improvements, Analyzer Cleanup (ryubing/ryujinx!47)
See merge request ryubing/ryujinx!47
2025-06-11 17:58:27 -05:00
d03ae9c164 fix: socket blocking flag is inverted when setting it (ryubing/ryujinx!57)
See merge request ryubing/ryujinx!57
2025-06-11 16:44:07 -05:00
90e9492f6c Update Korean translation (ryubing/ryujinx!56)
See merge request ryubing/ryujinx!56
2025-06-11 15:37:48 -05:00
512120db04 Work around Escape hotkey race with exit confirmation dialog
See merge request ryubing/ryujinx!54
2025-06-10 22:52:08 -05:00
90582e9e93 fix: crash caused by cursor overflow
See merge request ryubing/ryujinx!53
2025-06-10 16:34:12 -05:00
b97fae08b5 fix: use the correct font family for CJK characters
See merge request ryubing/ryujinx!52
2025-06-10 15:41:39 -05:00
eed6ef632d infra: [ci skip] update CHANGELOG.md 2025-06-09 19:57:31 -05:00
0409c15903 Remove GitHub updater support. 2025-06-09 19:51:53 -05:00
c58272ac20 infra: CI: Remove GitHub release uploading from Stable workflow. 2025-06-09 18:56:28 -05:00
9d83dfd19c misc: [ci skip] Missed the property part of _chosenProfile 2025-06-09 17:59:40 -05:00
ce31a47934 misc: Code styling changes & cleanups 2025-06-09 17:57:26 -05:00
d31d1f91cf Added the ability to switch between local and global input in the user configuration
See merge request ryubing/ryujinx!8
2025-06-09 17:24:24 -05:00
ef02194a77 Update: Compatibility list
See merge request ryubing/ryujinx!29
2025-06-09 02:54:45 -05:00
a16764d191 Moved "Graphics Backend Multitreading" item to Graphics API & Optimization section
See merge request ryubing/ryujinx!13
2025-06-09 02:37:49 -05:00
5108ab790f UI: RPC: [ci skip] Add BL2, BLTPS, and Minecraft Dungeons RPC images 2025-06-09 01:47:57 -05:00
71dc71fee8 infra: [ci skip] Remove duplicate GLI install in canary CI 2025-06-08 22:37:21 -05:00
c95bf748b2 infra: Update to Ryujinx.LibHac 0.20.0
This is identical to the previous version, it's just on NuGet.org so we can comment out the LibHacAlpha source in nuget.config.
2025-06-08 22:31:32 -05:00
b5e9acc50b misc: [ci skip] Cause GitHub fallback properly 2025-06-08 21:06:34 -05:00
e3fba4e32f docs: compat: further clarify the issue with 'FANTASY LIFE i: The Girl Who Steals Time' with 'crash' and 'vulkan-backend-bug' labels. 2025-06-08 20:44:01 -05:00
efa25d471e docs: compat: ingame: FANTASY LIFE i: The Girl Who Steals Time 2025-06-08 20:41:51 -05:00
b37aa61e47 infra: Remove GitHub uploading from Canary CI workflows 2025-06-08 17:55:36 -05:00
8feeb977b7 infra: [ci skip] fix canary changelog generation 2025-06-08 17:47:45 -05:00
b761a2c86d infra: Custom Update server instead of direct GitLab API calls
This reduces the amount of requests for an update from 3 if an update is needed, or 2 if not; to 1 if an update is needed, and none if an update is not. The difference comes from using this update server to check if an update is needed, and not GETing a snippet content for the release channels.
2025-06-08 17:37:34 -05:00
693837dca7 infra: [ci skip] make the canary release notes look nicer 2025-06-05 23:07:02 -05:00
70abff072b canary CI: checkout code before trying to get current revision 2025-06-05 20:56:17 -05:00
1e861b99a9 misc: Update LibHac
See merge request ryubing/libhac!3
2025-06-05 20:45:35 -05:00
13e404bde0 infra: [ci skip] Move tag creation to the end of the build process in CI 2025-06-05 01:57:21 -05:00
04561a0cd3 Vulkan: Use compute shader for non-indirect unsupported topology index buffer conversions
See merge request ryubing/ryujinx!5
2025-06-05 01:19:44 -05:00
0652d7e740 misc: readme: stable and canary release channels from gitlab 2025-06-04 23:18:44 -05:00
f2aea4fb22 misc: [ci skip] fix typo in comment & rename CheckForUpdateAsync 2025-06-04 21:05:54 -05:00
3950e8adff infra: Embed milestone description in stable release description in CI 2025-06-04 21:05:35 -05:00
0e84f2b1f0 infra: Send a Discord webhook message when a new build is available 2025-06-04 04:31:58 -05:00
051c794cc4 Use rcodesign for dylib signing where avaiilable and clear out all "._" files...
See merge request ryubing/ryujinx!14
2025-06-03 23:26:49 -05:00
053a9cb549 fix: use accurate length for enumerating
See merge request ryubing/ryujinx!49
2025-06-03 23:20:55 -05:00
d688fed7d2 missed the projects/ API endpoint part 2025-06-03 18:38:22 -05:00
8f5102aa2a infra: Add functionality to the CI to upload artifacts to this GitLab and make releases based on all files uploaded.
See merge request ryubing/ryujinx!48
2025-06-03 18:28:59 -05:00
78 changed files with 3988 additions and 1259 deletions

View File

@ -24,54 +24,6 @@ env:
RELEASE: 1
jobs:
tag:
name: Create tag
runs-on: ubuntu-24.04
steps:
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
shell: bash
- name: Install GitLabCli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitLab tag
run: gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateTag "Canary-${{ steps.version_info.outputs.build_version }}|master"
- name: Create release
uses: ncipollo/release-action@v1
with:
name: "Canary ${{ steps.version_info.outputs.build_version }}"
tag: ${{ steps.version_info.outputs.build_version }}
body: |
# Canary builds:
These builds are experimental and may sometimes not work, use [regular builds](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/latest) instead if that sounds like something you don't want to deal with.
| Platform | Artifact |
|--|--|
| Windows 64-bit | [Canary Windows Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-win_x64.zip) |
| Windows ARM 64-bit | [Canary Windows ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-win_arm64.zip) |
| Linux 64-bit | [Canary Linux Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz) |
| Linux ARM 64-bit | [Canary Linux ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-linux_arm64.tar.gz) |
| macOS | [Canary macOS Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz) |
**[Full Changelog](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})**
omitBodyDuringUpdate: true
owner: ${{ secrets.RC_OWNER }}
repo: ${{ secrets.RC_CANARY_NAME }}
token: ${{ secrets.ALT_RELEASE_TOKEN }}
release:
name: Release for ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
@ -79,7 +31,7 @@ jobs:
matrix:
platform:
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
- { name: win-arm64, os: windows-latest, zip_os_name: win_arm64 }
#- { 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 }
steps:
@ -123,7 +75,24 @@ jobs:
rm libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
popd
gh release download -R GreemDev/GLI -O gli.exe -p 'GitLabCli-win_x64.exe'
./gli.exe --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|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: Install GitLabCli
if: matrix.platform.os == 'ubuntu-latest'
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest'
@ -133,6 +102,8 @@ jobs:
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
popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz"
shell: bash
- name: Build AppImage (Linux)
@ -170,35 +141,11 @@ jobs:
mv Ryujinx.AppImage ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
mv Ryujinx.AppImage.zsync ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync
popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage"
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync"
shell: bash
- name: Pushing new release
uses: ncipollo/release-action@v1
with:
name: ${{ steps.version_info.outputs.build_version }}
artifacts: "release_output/*.tar.gz,release_output/*.zip,release_output/*AppImage*"
tag: ${{ steps.version_info.outputs.build_version }}
body: |
# Canary builds:
These builds are experimental and may sometimes not work, use [regular builds](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/latest) instead if that sounds like something you don't want to deal with.
| Platform | Artifact |
|--|--|
| Windows 64-bit | [Canary Windows Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-win_x64.zip) |
| Windows ARM 64-bit | [Canary Windows ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-win_arm64.zip) |
| Linux 64-bit | [Canary Linux Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz) |
| Linux ARM 64-bit | [Canary Linux ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-linux_arm64.tar.gz) |
| macOS | [Canary macOS Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_CANARY_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz) |
**[Full Changelog](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})**
omitBodyDuringUpdate: true
allowUpdates: true
replacesArtifacts: true
owner: ${{ secrets.RC_OWNER }}
repo: ${{ secrets.RC_CANARY_NAME }}
token: ${{ secrets.ALT_RELEASE_TOKEN }}
macos_release:
name: Release MacOS universal
runs-on: ubuntu-24.04
@ -215,6 +162,16 @@ jobs:
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install GitLabCli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install rcodesign
run: |
mkdir -p $HOME/.bin
@ -246,17 +203,47 @@ 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 --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=UploadGenericPackage "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz"
- name: Pushing new release
uses: ncipollo/release-action@v1
with:
name: "Canary ${{ steps.version_info.outputs.build_version }}"
artifacts: "publish_ava/*.tar.gz"
tag: ${{ steps.version_info.outputs.build_version }}
body: ""
omitBodyDuringUpdate: true
allowUpdates: true
replacesArtifacts: true
owner: ${{ secrets.RC_OWNER }}
repo: ${{ secrets.RC_CANARY_NAME }}
token: ${{ secrets.ALT_RELEASE_TOKEN }}
create_gitlab_release:
name: Create GitLab Release
runs-on: ubuntu-24.04
needs:
- macos_release
- release
steps:
- uses: actions/checkout@v4
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Install GitLabCli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create tag
run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateTag "Canary-${{ steps.version_info.outputs.build_version }}|${{ steps.version_info.outputs.git_short_hash }}"
- name: Create release
run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=CreateReleaseFromGenericPackageFiles "Ryubing-Canary|${{ steps.version_info.outputs.build_version }}|main|Canary ${{ steps.version_info.outputs.build_version }}|**Full Changelog:** [${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
- name: Send notification webhook
run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/canary --command=SendUpdateMessage "${{ steps.version_info.outputs.build_version }}|FF4500|${{ secrets.CANARY_DISCORD_WEBHOOK }}|https://avatars.githubusercontent.com/u/192939710?s=200&v=4|false"
- name: Notify update server of new builds
run: |
curl 'https://update.ryujinx.app/api/v1/admin/refresh_cache?rc=canary' -X PATCH -H 'accept: */*' -H 'Authorization: ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}'

View File

@ -67,8 +67,21 @@ jobs:
gh release download -R GreemDev/GLI -O gli.exe -p 'GitLabCli-win_x64.exe'
./gli.exe --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip"
./gli.exe --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip"
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install GitLabCli
if: matrix.platform.os == 'ubuntu-latest'
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest'
@ -79,13 +92,7 @@ jobs:
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
popd
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz"
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz"
shell: bash
- name: Build AppImage (Linux)
@ -124,8 +131,8 @@ jobs:
mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync
popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage"
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync"
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage"
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync"
shell: bash
macos_release:
@ -183,7 +190,7 @@ 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 --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "ryubing|${{ steps.version_info.outputs.build_version }}|publish_ava/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz"
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz"
create_gitlab_release:
name: Create GitLab Release
@ -192,6 +199,8 @@ jobs:
- macos_release
- release
steps:
- uses: actions/checkout@v4
- name: Get version info
id: version_info
run: |
@ -212,4 +221,4 @@ jobs:
- name: Create release
run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateReleaseFromGenericPackageFiles "ryubing|${{ steps.version_info.outputs.build_version }}|${{ steps.version_info.outputs.git_short_hash }}|test|THIS IS NOT INTENDED FOR END USER USAGE"
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateReleaseFromGenericPackageFiles "Ryubing|${{ steps.version_info.outputs.build_version }}|${{ steps.version_info.outputs.git_short_hash }}|test|THIS IS NOT INTENDED FOR END USER USAGE"

View File

@ -14,51 +14,6 @@ env:
RELEASE: 1
jobs:
tag:
name: Create tag
runs-on: ubuntu-24.04
steps:
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
shell: bash
- name: Install GitLabCli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitLab tag
run: gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateTag "${{ steps.version_info.outputs.build_version }}|master"
- name: Create release
uses: ncipollo/release-action@v1
with:
name: ${{ steps.version_info.outputs.build_version }}
tag: ${{ steps.version_info.outputs.build_version }}
body: |
# Stable builds:
| Platform | Artifact |
|--|--|
| Windows 64-bit | [Stable Windows Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-win_x64.zip) |
| Windows ARM 64-bit | [Stable Windows ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-win_arm64.zip) |
| Linux 64-bit | [Stable Linux Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz) |
| Linux ARM 64-bit | [Stable Linux ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_arm64.tar.gz) |
| macOS | [Stable macOS Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz) |
**[Full Changelog](https://git.ryujinx.app/ryubing/ryujinx/-/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }})**
omitBodyDuringUpdate: true
owner: ${{ secrets.RC_OWNER }}
repo: ${{ secrets.RC_STABLE_NAME }}
token: ${{ secrets.ALT_RELEASE_TOKEN }}
release:
name: Release for ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
@ -66,7 +21,7 @@ jobs:
matrix:
platform:
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
- { name: win-arm64, os: windows-latest, zip_os_name: win_arm64 }
#- { 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 }
steps:
@ -109,7 +64,24 @@ jobs:
rm libarmeilleure-jitsupport.dylib
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
popd
gh release download -R GreemDev/GLI -O gli.exe -p 'GitLabCli-win_x64.exe'
./gli.exe --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip"
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install GitLabCli
if: matrix.platform.os == 'ubuntu-latest'
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Packing Linux builds
if: matrix.platform.os == 'ubuntu-latest'
@ -119,7 +91,11 @@ jobs:
chmod +x Ryujinx.sh Ryujinx
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|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: matrix.platform.os == 'ubuntu-latest'
@ -156,32 +132,11 @@ jobs:
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync
popd
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage"
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync"
shell: bash
- name: Pushing new release
uses: ncipollo/release-action@v1
with:
name: ${{ steps.version_info.outputs.build_version }}
artifacts: "release_output/*.tar.gz,release_output/*.zip,release_output/*AppImage*"
tag: ${{ steps.version_info.outputs.build_version }}
body: |
# Stable builds:
| Platform | Artifact |
|--|--|
| Windows 64-bit | [Stable Windows Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-win_x64.zip) |
| Windows ARM 64-bit | [Stable Windows ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-win_arm64.zip) |
| Linux 64-bit | [Stable Linux Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_x64.tar.gz) |
| Linux ARM 64-bit | [Stable Linux ARM Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-linux_arm64.tar.gz) |
| macOS | [Stable macOS Artifact](https://github.com/${{ secrets.RC_OWNER }}/${{ secrets.RC_STABLE_NAME }}/releases/download/${{ steps.version_info.outputs.build_version }}/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz) |
**[Full Changelog](https://git.ryujinx.app/ryubing/ryujinx/-/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }})**
omitBodyDuringUpdate: true
allowUpdates: true
replacesArtifacts: true
owner: ${{ secrets.RC_OWNER }}
repo: ${{ secrets.RC_STABLE_NAME }}
token: ${{ secrets.ALT_RELEASE_TOKEN }}
macos_release:
name: Release MacOS universal
runs-on: ubuntu-24.04
@ -198,6 +153,16 @@ jobs:
chmod +x llvm.sh
sudo ./llvm.sh 17
- name: Install GitLabCli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install rcodesign
run: |
mkdir -p $HOME/.bin
@ -227,17 +192,43 @@ 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 --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=UploadGenericPackage "Ryubing|${{ steps.version_info.outputs.build_version }}|publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz"
- name: Pushing new release
uses: ncipollo/release-action@v1
with:
name: ${{ steps.version_info.outputs.build_version }}
artifacts: "publish/*.tar.gz"
tag: ${{ steps.version_info.outputs.build_version }}
body: ""
omitBodyDuringUpdate: true
allowUpdates: true
replacesArtifacts: true
owner: ${{ secrets.RC_OWNER }}
repo: ${{ secrets.RC_STABLE_NAME }}
token: ${{ secrets.ALT_RELEASE_TOKEN }}
create_gitlab_release:
name: Create GitLab Release
runs-on: ubuntu-24.04
needs:
- macos_release
- release
steps:
- uses: actions/checkout@v4
- name: Get version info
id: version_info
run: |
echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT
echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
shell: bash
- name: Install GitLabCli
run: |
mkdir -p $HOME/.bin
gh release download -R GreemDev/GLI -O gli -p 'GitLabCli-linux_x64'
chmod +x gli
mv gli $HOME/.bin/
echo "$HOME/.bin" >> $GITHUB_PATH
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release
run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=CreateReleaseFromGenericPackageFiles "Ryubing|${{ steps.version_info.outputs.build_version }}|${{ steps.version_info.outputs.git_short_hash }}|${{ steps.version_info.outputs.build_version }}|msd:${{ steps.version_info.outputs.build_version }}"
- name: Send notification webhook
run: |
gli --access-token=${{ secrets.GITLAB_TOKEN }} --project=ryubing/ryujinx --command=SendUpdateMessage "${{ steps.version_info.outputs.build_version }}|32cd32|${{ secrets.STABLE_DISCORD_WEBHOOK }}|https://avatars.githubusercontent.com/u/192939710?s=200&v=4|false"
- name: Notify update server of new builds
run: |
curl 'https://update.ryujinx.app/api/v1/admin/refresh_cache?rc=stable' -X PATCH -H 'accept: */*' -H 'Authorization: ${{ secrets.UPDATE_SERVER_ADMIN_TOKEN }}'

View File

@ -2,20 +2,17 @@
All updates to this Ryujinx branch will be documented in this file.
## [1.3.2](<https://git.ryujinx.app/ryubing/ryujinx/-/releases/1.3.2>) - 2025-06-09
## [1.3.1](<https://git.ryujinx.app/ryubing/ryujinx/-/releases/1.3.1>) - 2025-04-23
A list of notable changes can be found on the release linked in the version number above.
## [1.2.86](<https://github.com/Ryubing/Stable-Releases/releases/tag/1.2.86>) - 2025-03-13
A list of notable changes can be found on the release linked in the version number above.
## [1.2.82](<https://web.archive.org/web/20250312010534/https://github.com/Ryubing/Ryujinx/releases/tag/1.2.82>) - 2025-02-16
A list of notable changes can be found on the release linked in the version number above.
## [1.2.80-81](<https://web.archive.org/web/20250302064257/https://github.com/Ryubing/Ryujinx/releases/tag/1.2.81>) - 2025-01-22
A list of notable changes can be found on the release linked in the version number above.
## [1.2.78](<https://web.archive.org/web/20250301174537/https://github.com/Ryubing/Ryujinx/releases/tag/1.2.78>) - 2024-12-19
A list of notable changes can be found on the release linked in the version number above.
## [1.2.73-1.2.76](<https://web.archive.org/web/20250209202612/https://github.com/Ryubing/Ryujinx/releases/tag/1.2.76>) - 2024-11-19
A list of notable changes can be found on the release linked in the version number above.

View File

@ -40,8 +40,10 @@
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.2-build3" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.20.0-alpha.107" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.20.0" />
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.29" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.29" />
<PackageVersion Include="Gommon" Version="2.7.1.1" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.6.0" />

View File

@ -7,8 +7,8 @@
# Ryujinx
[![Latest release](https://img.shields.io/github/v/release/Ryubing/Stable-Releases?label=stable)](https://github.com/Ryubing/Stable-Releases/releases/latest)
[![Latest canary release](https://img.shields.io/github/v/release/Ryubing/Canary-Releases?label=canary)](https://github.com/Ryubing/Canary-Releases/releases/latest)
[![Latest release](https://img.shields.io/gitlab/v/release/ryubing%2Fryujinx?gitlab_url=https%3A%2F%2Fgit.ryujinx.app&label=stable&color=32cd32)](https://git.ryujinx.app/ryubing/ryujinx/-/releases)
[![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://git.ryujinx.app/ryubing/canary/-/releases)
<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">
@ -31,7 +31,7 @@
<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/ryubing/ryujinx/-/wikis/home">Wiki tab</a>.
Guides and documentation can be found on the <a href="https://git.ryujinx.app/groups/ryubing/-/wikis/home">Wiki tab</a>.
</p>
<p align="center">
@ -49,13 +49,13 @@ 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 latest stable release [here](https://github.com/Ryubing/Stable-Releases/releases/latest).
You can find the stable releases [here](https://git.ryujinx.app/ryubing/ryujinx/-/releases).
Canary builds are compiled automatically for each commit on the `master` branch.
While we strive to ensure optimal stability and performance prior to pushing an update, these builds **may be unstable or completely broken**.
These canary builds are only recommended for experienced users.
You can find the latest canary release [here](https://github.com/Ryubing/Canary-Releases/releases/latest).
You can find the canary releases [here](https://git.ryujinx.app/ryubing/canary/-/releases).
## Documentation
@ -111,7 +111,7 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY
## Credits
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
- [LibHac](https://git.ryujinx.app/ryubing/libhac) is used for our file-system.
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.

View File

@ -77,6 +77,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Gene
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.BuildValidationTasks", "src\Ryujinx.BuildValidationTasks\Ryujinx.BuildValidationTasks.csproj", "{4A89A234-4F19-497D-A576-DDE8CDFC5B22}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
@ -84,10 +86,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.github\workflows\canary.yml = .github\workflows\canary.yml
Directory.Packages.props = Directory.Packages.props
.github\workflows\release.yml = .github\workflows\release.yml
nuget.config = nuget.config
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.BuildValidationTasks", "src\Ryujinx.BuildValidationTasks\Ryujinx.BuildValidationTasks.csproj", "{4A89A234-4F19-497D-A576-DDE8CDFC5B22}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

File diff suppressed because it is too large Load Diff

View File

@ -33,23 +33,29 @@ echo -n "APPL????" > "$APP_BUNDLE_DIRECTORY/Contents/PkgInfo"
echo "Running bundle fix up python script"
python3 bundle_fix_up.py "$APP_BUNDLE_DIRECTORY" MacOS/Ryujinx
# Resign all dyplib files as ad-hoc after changing them
find "$APP_BUNDLE_DIRECTORY/Contents/Frameworks" -type f -name "*.dylib" -exec codesign --force --sign - {} \;
# Now sign it
echo "Starting signing process"
if ! [ -x "$(command -v codesign)" ];
then
if ! [ -x "$(command -v rcodesign)" ];
then
echo "Cannot find rcodesign on your system, please install rcodesign."
echo "Cannot find rcodesign on your system, please install rcodesign and ensure it is in your search path."
exit 1
fi
# cargo install apple-codesign
echo "Using rcodesign for ad-hoc signing"
echo "Resigning all frameworks dylib files as ad-hoc"
find "$APP_BUNDLE_DIRECTORY/Contents/Frameworks" -type f -name "*.dylib" -exec rcodesign sign {} \;
echo "Signing app bundle as ad-hoc"
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$APP_BUNDLE_DIRECTORY"
else
echo "Using codesign for ad-hoc signing"
echo "Resigning all frameworks dylib files as ad-hoc"
find "$APP_BUNDLE_DIRECTORY/Contents/Frameworks" -type f -name "*.dylib" -exec codesign --force --sign - {} \;
echo "Signing app bundle as ad-hoc"
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$APP_BUNDLE_DIRECTORY"
fi

View File

@ -20,6 +20,18 @@ SOURCE_REVISION_ID=$6
CONFIGURATION=$7
CANARY=$8
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
if [ "$CANARY" == "1" ]; then
RELEASE_TAR_FILE_NAME=ryujinx-canary-$VERSION-macos_universal.app.tar
elif [ "$VERSION" == "1.1.0" ]; then

View File

@ -20,6 +20,18 @@ SOURCE_REVISION_ID=$6
CONFIGURATION=$7
CANARY=$8
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
if [ "$CANARY" == "1" ]; then
RELEASE_TAR_FILE_NAME=nogui-ryujinx-canary-$VERSION-macos_universal.tar
elif [ "$VERSION" == "1.1.0" ]; then

View File

@ -1125,6 +1125,7 @@
0100034012606000,"Family Mysteries: Poisonous Promises",audio;crash,menus,2021-11-26 12:35:06
010017C012726000,"Fantasy Friends",,playable,2022-10-17 19:42:39
0100767008502000,"FANTASY HERO unsigned legacy",,playable,2022-07-26 12:28:52
0100755017EE0000,"FANTASY LIFE i: The Girl Who Steals Time",gpu;crash;vulkan-backend-bug,ingame,2025-06-08 20:41:00
0100944003820000,"Fantasy Strike",online,playable,2021-02-27 01:59:18
01000E2012F6E000,"Fantasy Tavern Sextet -Vol.1 New World Days-",gpu;crash;Needs Update,ingame,2022-12-05 16:48:00
01005C10136CA000,"Fantasy Tavern Sextet -Vol.2 Adventurer's Days-",gpu;slow;crash,ingame,2021-11-06 02:57:29
@ -2745,6 +2746,7 @@
01005D701264A000,"SpyHack",,playable,2021-04-15 10:53:51
010077B00E046000,"Spyro™ Reignited Trilogy",nvdec;UE4,playable,2022-09-11 18:38:33
0100085012A0E000,"Squeakers",,playable,2020-12-13 12:13:05
0100E1D01EB2E000,"Squeakross: Home Squeak Home",,playable,2025-06-16 02:02:00
010009300D31C000,"Squidgies Takeover",,playable,2020-07-20 22:28:08
0100FCD0102EC000,"Squidlit",,playable,2020-08-06 12:38:32
0100EBF00E702000,"STAR OCEAN First Departure R",nvdec,playable,2021-07-05 19:29:16
@ -3015,6 +3017,7 @@
01009B101044C000,"The Legend of Heroes: Trails of Cold Steel III Demo",demo;nvdec,playable,2021-04-23 01:07:32
0100D3C010DE8000,"The Legend of Heroes: Trails of Cold Steel IV",nvdec,playable,2021-04-23 14:01:05
01005E5013862000,"THE LEGEND OF HEROES: ZERO NO KISEKI KAI [英雄傳說 零之軌跡:改]",crash,nothing,2021-09-30 14:41:07
01009C901ACEE000,"The Legend of Nayuta: Boundless Trails",,ingame,2025-06-12 15:47
01008CF01BAAC000,"The Legend of Zelda Echoes of Wisdom",nvdec;ASTC;intel-vendor-bug,playable,2024-10-01 14:11:01
0100509005AF2000,"The Legend of Zelda: Breath of the Wild Demo",demo,ingame,2022-12-24 05:02:58
01007EF00011E000,"The Legend of Zelda™: Breath of the Wild",gpu;amd-vendor-bug;mac-bug,ingame,2024-09-23 19:35:46

1 title_id game_name labels status last_updated
1125 0100034012606000 Family Mysteries: Poisonous Promises audio;crash menus 2021-11-26 12:35:06
1126 010017C012726000 Fantasy Friends playable 2022-10-17 19:42:39
1127 0100767008502000 FANTASY HERO ~unsigned legacy~ playable 2022-07-26 12:28:52
1128 0100755017EE0000 FANTASY LIFE i: The Girl Who Steals Time gpu;crash;vulkan-backend-bug ingame 2025-06-08 20:41:00
1129 0100944003820000 Fantasy Strike online playable 2021-02-27 01:59:18
1130 01000E2012F6E000 Fantasy Tavern Sextet -Vol.1 New World Days- gpu;crash;Needs Update ingame 2022-12-05 16:48:00
1131 01005C10136CA000 Fantasy Tavern Sextet -Vol.2 Adventurer's Days- gpu;slow;crash ingame 2021-11-06 02:57:29
2746 01005D701264A000 SpyHack playable 2021-04-15 10:53:51
2747 010077B00E046000 Spyro™ Reignited Trilogy nvdec;UE4 playable 2022-09-11 18:38:33
2748 0100085012A0E000 Squeakers playable 2020-12-13 12:13:05
2749 0100E1D01EB2E000 Squeakross: Home Squeak Home playable 2025-06-16 02:02:00
2750 010009300D31C000 Squidgies Takeover playable 2020-07-20 22:28:08
2751 0100FCD0102EC000 Squidlit playable 2020-08-06 12:38:32
2752 0100EBF00E702000 STAR OCEAN First Departure R nvdec playable 2021-07-05 19:29:16
3017 01009B101044C000 The Legend of Heroes: Trails of Cold Steel III Demo demo;nvdec playable 2021-04-23 01:07:32
3018 0100D3C010DE8000 The Legend of Heroes: Trails of Cold Steel IV nvdec playable 2021-04-23 14:01:05
3019 01005E5013862000 THE LEGEND OF HEROES: ZERO NO KISEKI KAI [英雄傳說 零之軌跡:改] crash nothing 2021-09-30 14:41:07
3020 01009C901ACEE000 The Legend of Nayuta: Boundless Trails ingame 2025-06-12 15:47
3021 01008CF01BAAC000 The Legend of Zelda Echoes of Wisdom nvdec;ASTC;intel-vendor-bug playable 2024-10-01 14:11:01
3022 0100509005AF2000 The Legend of Zelda: Breath of the Wild Demo demo ingame 2022-12-24 05:02:58
3023 01007EF00011E000 The Legend of Zelda™: Breath of the Wild gpu;amd-vendor-bug;mac-bug ingame 2024-09-23 19:35:46

View File

@ -4,20 +4,22 @@
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="LibHacAlpha" value="https://git.ryujinx.app/api/v4/projects/17/packages/nuget/index.json" />
<add key="RyubingPkgs" value="https://git.ryujinx.app/api/v4/projects/1/packages/nuget/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" />
</packageSources>
<!-- Define mappings by adding package patterns beneath the target source. -->
<!-- Ryujinx.LibHac packages will be restored from LibHacAlpha,
everything else from nuget.org. -->
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
<!-- These are defined and .NET still yells about multiple package sources with no mappings. Not sure what to do, this is in the docs lol -->
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="LibHacAlpha">
<package pattern="Ryujinx.LibHac" />
<packageSource key="Ryujinx.UpdateClient">
<package pattern="Ryujinx.UpdateClient" />
<package pattern="Ryujinx.Systems.Update.Common" />
</packageSource>
<!--<packageSource key="LibHacAlpha">
<package pattern="Ryujinx.LibHac" />
</packageSource>-->
</packageSourceMapping>
</configuration>

View File

@ -193,7 +193,7 @@ namespace ARMeilleure.Translation.PTC
_infosStream.Seek(0L, SeekOrigin.Begin);
bool foundBadFunction = false;
for (int index = 0; index < GetEntriesCount(); index++)
for (int index = 0; index < _infosStream.Length / Unsafe.SizeOf<InfoEntry>(); index++)
{
InfoEntry infoEntry = DeserializeStructure<InfoEntry>(_infosStream);
foreach (ulong address in blacklist)

View File

@ -0,0 +1,200 @@
using System;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
namespace Ryujinx.Common.Configuration.Hid
{
/// <summary>
/// Provides default input configurations for keyboard and controller devices
/// </summary>
public static class DefaultInputConfigurationProvider
{
/// <summary>
/// Creates a default keyboard input configuration
/// </summary>
/// <param name="id">Device ID</param>
/// <param name="name">Device name</param>
/// <param name="playerIndex">Player index</param>
/// <param name="controllerType">Controller type (defaults to ProController)</param>
/// <returns>Default keyboard input configuration</returns>
public static StandardKeyboardInputConfig CreateDefaultKeyboardConfig(string id, string name, PlayerIndex playerIndex, ControllerType controllerType = ControllerType.ProController)
{
return new StandardKeyboardInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.WindowKeyboard,
Id = id,
Name = name,
ControllerType = ControllerType.ProController,
PlayerIndex = playerIndex,
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
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<Key>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
{
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<Key>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
},
};
}
/// <summary>
/// Creates a default controller input configuration
/// </summary>
/// <param name="id">Device ID</param>
/// <param name="name">Device name</param>
/// <param name="playerIndex">Player index</param>
/// <param name="isNintendoStyle">Whether to use Nintendo-style button mapping</param>
/// <returns>Default controller input configuration</returns>
public static StandardControllerInputConfig CreateDefaultControllerConfig(string id, string name, PlayerIndex playerIndex, bool isNintendoStyle = false)
{
// Split the ID for controller configs
string cleanId = id.Split(" ")[0];
return new StandardControllerInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL2,
Id = cleanId,
Name = name,
ControllerType = ControllerType.ProController,
PlayerIndex = playerIndex,
DeadzoneLeft = 0.1f,
DeadzoneRight = 0.1f,
RangeLeft = 1.0f,
RangeRight = 1.0f,
TriggerThreshold = 0.5f,
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
{
DpadUp = ConfigGamepadInputId.DpadUp,
DpadDown = ConfigGamepadInputId.DpadDown,
DpadLeft = ConfigGamepadInputId.DpadLeft,
DpadRight = ConfigGamepadInputId.DpadRight,
ButtonMinus = ConfigGamepadInputId.Minus,
ButtonL = ConfigGamepadInputId.LeftShoulder,
ButtonZl = ConfigGamepadInputId.LeftTrigger,
ButtonSl = ConfigGamepadInputId.Unbound,
ButtonSr = ConfigGamepadInputId.Unbound,
},
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Left,
StickButton = ConfigGamepadInputId.LeftStick,
InvertStickX = false,
InvertStickY = false,
},
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
{
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
ButtonPlus = ConfigGamepadInputId.Plus,
ButtonR = ConfigGamepadInputId.RightShoulder,
ButtonZr = ConfigGamepadInputId.RightTrigger,
ButtonSl = ConfigGamepadInputId.Unbound,
ButtonSr = ConfigGamepadInputId.Unbound,
},
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Right,
StickButton = ConfigGamepadInputId.RightStick,
InvertStickX = false,
InvertStickY = false,
},
Motion = new StandardMotionConfigController
{
EnableMotion = true,
MotionBackend = MotionInputBackendType.GamepadDriver,
GyroDeadzone = 1,
Sensitivity = 100,
},
Rumble = new RumbleConfigController
{
EnableRumble = false,
WeakRumble = 1f,
StrongRumble = 1f,
},
Led = new LedConfigController
{
EnableLed = false,
TurnOffLed = false,
UseRainbow = false,
LedColor = 0xFFFFFFFF,
}
};
}
/// <summary>
/// Gets the short name of a gamepad by removing SDL prefix and truncating if too long
/// </summary>
/// <param name="name">Full gamepad name</param>
/// <param name="maxLength">Maximum length before truncation (default: 50)</param>
/// <returns>Short gamepad name</returns>
public static string GetShortGamepadName(string name, int maxLength = 50)
{
const string SdlGamepadNamePrefix = "SDL2 Gamepad ";
const string Ellipsis = "...";
// First remove SDL prefix if present
string shortName = name;
if (name.StartsWith(SdlGamepadNamePrefix))
{
shortName = name[SdlGamepadNamePrefix.Length..];
}
// Then truncate if too long
if (shortName.Length > maxLength)
{
return $"{shortName.AsSpan(0, maxLength - Ellipsis.Length)}{Ellipsis}";
}
return shortName;
}
/// <summary>
/// Determines if a controller uses Nintendo-style button mapping
/// </summary>
/// <param name="name">Controller name</param>
/// <returns>True if Nintendo-style mapping should be used</returns>
public static bool IsNintendoStyleController(string name)
{
return name.Contains("Nintendo");
}
}
}

View File

@ -15,5 +15,14 @@ namespace Ryujinx.Common.Configuration.Hid
public Key CustomVSyncIntervalDecrement { get; set; }
public Key TurboMode { get; set; }
public bool TurboModeWhileHeld { get; set; }
public Key CycleInputDevicePlayer1 { get; set; }
public Key CycleInputDevicePlayer2 { get; set; }
public Key CycleInputDevicePlayer3 { get; set; }
public Key CycleInputDevicePlayer4 { get; set; }
public Key CycleInputDevicePlayer5 { get; set; }
public Key CycleInputDevicePlayer6 { get; set; }
public Key CycleInputDevicePlayer7 { get; set; }
public Key CycleInputDevicePlayer8 { get; set; }
public Key CycleInputDeviceHandheld { get; set; }
}
}

View File

@ -32,59 +32,11 @@ namespace Ryujinx.Common
public static string Version => IsValid ? BuildVersion : Assembly.GetEntryAssembly()!.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
public static string GetChangelogUrl(Version currentVersion, Version newVersion, ReleaseChannels.Channel releaseChannel) =>
public static string GetChangelogUrl(Version currentVersion, Version newVersion) =>
IsCanaryBuild
? $"https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-{currentVersion}...Canary-{newVersion}"
: GetChangelogForVersion(newVersion, releaseChannel);
public static string GetChangelogForVersion(Version version, ReleaseChannels.Channel releaseChannel) =>
$"https://github.com/{releaseChannel}/releases/{version}";
public static async Task<ReleaseChannels> GetReleaseChannelsAsync(HttpClient httpClient)
{
ReleaseChannelPair releaseChannelPair = await httpClient.GetFromJsonAsync("https://ryujinx.app/api/release-channels", ReleaseChannelPairContext.Default.ReleaseChannelPair);
return new ReleaseChannels(releaseChannelPair);
}
: $"https://git.ryujinx.app/ryubing/ryujinx/-/releases/{newVersion}";
}
public readonly struct ReleaseChannels
{
internal ReleaseChannels(ReleaseChannelPair channelPair)
{
Stable = new Channel(channelPair.Stable);
Canary = new Channel(channelPair.Canary);
}
public readonly Channel Stable;
public readonly Channel Canary;
public readonly struct Channel
{
public Channel(string raw)
{
string[] parts = raw.Split('/');
Owner = parts[0];
Repo = parts[1];
}
public readonly string Owner;
public readonly string Repo;
public override string ToString() => $"{Owner}/{Repo}";
public string GetLatestReleaseApiUrl() =>
$"https://api.github.com/repos/{ToString()}/releases/latest";
}
}
[JsonSerializable(typeof(ReleaseChannelPair))]
partial class ReleaseChannelPairContext : JsonSerializerContext;
class ReleaseChannelPair
{
[JsonPropertyName("stable")]
public string Stable { get; set; }
[JsonPropertyName("canary")]
public string Canary { get; set; }
}
}

View File

@ -133,7 +133,6 @@ namespace Ryujinx.Common
"0100c1f0051b6000", // Donkey Kong Country: Tropical Freeze
"0100ed000d390000", // Dr. Kawashima's Brain Training
"010067b017588000", // Endless Ocean Luminous
"0100d2f00d5c0000", // Nintendo Switch Sports
"01006b5012b32000", // Part Time UFO
"0100704000B3A000", // Snipperclips
"01006a800016e000", // Super Smash Bros. Ultimate
@ -169,6 +168,8 @@ namespace Ryujinx.Common
"010056e00853a000", // A Hat in Time
"0100fd1014726000", // Baldurs Gate: Dark Alliance
"01008c2019598000", // Bluey: The Video Game
"010096f00ff22000", // Borderlands 2: Game of the Year Edition
"010007400ff24000", // Borderlands: The Pre-Sequel Ultimate Edition
"0100c6800b934000", // Brawlhalla
"0100dbf01000a000", // Burnout Paradise Remastered
"0100744001588000", // Cars 3: Driven to Win
@ -194,6 +195,7 @@ namespace Ryujinx.Common
"01008d100d43e000", // Saints Row IV
"0100de600beee000", // Saints Row: The Third - The Full Package
"01001180021fa000", // Shovel Knight: Specter of Torment
"0100e1D01eb2e000", // Squeakross: Home Squeak Home
"0100e65002bb8000", // Stardew Valley
"0100d7a01b7a2000", // Star Wars: Bounty Hunter
"0100800015926000", // Suika Game

View File

@ -41,5 +41,10 @@ namespace Ryujinx.Graphics.GAL.Multithreading
public void SetScalingFilterLevel(float level) { }
public void SetColorSpacePassthrough(bool colorSpacePassthroughEnabled) { }
/// <summary>
/// Gets the underlying implementation window for direct access
/// </summary>
public IWindow BaseWindow => _impl.Window;
}
}

View File

@ -40,6 +40,7 @@ namespace Ryujinx.Graphics.Gpu
/// GPU synchronization manager.
/// </summary>
public SynchronizationManager Synchronization { get; }
public IOverlayManager OverlayManager { get; }
/// <summary>
/// Presentation window.
@ -121,7 +122,9 @@ namespace Ryujinx.Graphics.Gpu
/// Creates a new instance of the GPU emulation context.
/// </summary>
/// <param name="renderer">Host renderer</param>
public GpuContext(IRenderer renderer, DirtyHacks hacks)
/// <param name="hacks">Enabled dirty hacks</param>
/// <param name="overlayManager">Overlay manager for rendering overlays</param>
public GpuContext(IRenderer renderer, DirtyHacks hacks, IOverlayManager overlayManager)
{
Renderer = renderer;
@ -129,6 +132,8 @@ namespace Ryujinx.Graphics.Gpu
Synchronization = new SynchronizationManager();
OverlayManager = overlayManager;
Window = new Window(this);
HostInitalized = new ManualResetEvent(false);
@ -462,6 +467,8 @@ namespace Ryujinx.Graphics.Gpu
RunDeferredActions();
Renderer.Dispose();
OverlayManager.Dispose();
}
}
}

View File

@ -0,0 +1,54 @@
using SkiaSharp;
using System;
namespace Ryujinx.Graphics.Gpu
{
/// <summary>
/// Interface for overlay functionality
/// </summary>
public interface IOverlay : IDisposable
{
/// <summary>
/// Name of the overlay
/// </summary>
string Name { get; set; }
/// <summary>
/// Whether the overlay is visible
/// </summary>
bool IsVisible { get; set; }
/// <summary>
/// Opacity of the overlay (0.0 to 1.0)
/// </summary>
float Opacity { get; set; }
/// <summary>
/// X position of the overlay
/// </summary>
float X { get; set; }
/// <summary>
/// Y position of the overlay
/// </summary>
float Y { get; set; }
/// <summary>
/// Z-index for overlay ordering
/// </summary>
int ZIndex { get; set; }
/// <summary>
/// Update overlay (for animations)
/// </summary>
/// <param name="deltaTime">Time elapsed since last update</param>
/// <param name="screenSize">Current screen size</param>
void Update(float deltaTime, SKSize screenSize = default);
/// <summary>
/// Render this overlay
/// </summary>
/// <param name="canvas">The canvas to render to</param>
void Render(SKCanvas canvas);
}
}

View File

@ -0,0 +1,30 @@
using SkiaSharp;
using System;
namespace Ryujinx.Graphics.Gpu
{
/// <summary>
/// Interface for overlay management functionality
/// </summary>
public interface IOverlayManager : IDisposable
{
/// <summary>
/// Add an overlay to the manager
/// </summary>
/// <param name="overlay">The overlay to add</param>
void AddOverlay(IOverlay overlay);
/// <summary>
/// Update all overlays (for animations)
/// </summary>
/// <param name="deltaTime">Time elapsed since last update</param>
/// <param name="screenSize">Current screen size</param>
void Update(float deltaTime, SKSize screenSize = default);
/// <summary>
/// Render all visible overlays
/// </summary>
/// <param name="canvas">The canvas to render to</param>
void Render(SKCanvas canvas);
}
}

View File

@ -14,4 +14,8 @@
<ProjectReference Include="..\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" />
</ItemGroup>
</Project>

View File

@ -1,10 +1,14 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Graphics.Texture;
using Ryujinx.Memory.Range;
using SkiaSharp;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
namespace Ryujinx.Graphics.Gpu
@ -15,6 +19,7 @@ namespace Ryujinx.Graphics.Gpu
public class Window
{
private readonly GpuContext _context;
private DateTime? _lastUpdateTime = null;
/// <summary>
/// Texture presented on the window.
@ -207,6 +212,9 @@ namespace Ryujinx.Graphics.Gpu
texture.SynchronizeMemory();
// Add overlays by modifying texture data directly
AddOverlaysToTexture(texture, pt.Crop);
ImageCrop crop = new(
(int)(pt.Crop.Left * texture.ScaleFactor),
(int)MathF.Ceiling(pt.Crop.Right * texture.ScaleFactor),
@ -244,6 +252,99 @@ namespace Ryujinx.Graphics.Gpu
}
}
/// <summary>
/// Add overlay to the overlay manager
/// </summary>
public void AddOverlay(IOverlay overlay)
{
_context.OverlayManager.AddOverlay(overlay);
}
/// <summary>
/// Add overlays to the texture using SkiaSharp
/// </summary>
/// <param name="texture">The texture to modify</param>
/// <param name="crop">The crop information containing flip flags</param>
private void AddOverlaysToTexture(Image.Texture texture, ImageCrop crop)
{
try
{
DateTime currentTime = DateTime.UtcNow;
if (_lastUpdateTime != null)
{
// Calculate delta time for lifespan updates
float deltaTime = (float)(currentTime - _lastUpdateTime.Value).TotalSeconds;
_context.OverlayManager.Update(deltaTime, new SKSize(texture.Info.Width, texture.Info.Height));
}
// Update overlay animations
_lastUpdateTime = currentTime;
// Get texture data from host texture
using var pinnedData = texture.HostTexture.GetData();
var data = pinnedData.Get().ToArray();
if (data == null || data.Length == 0)
return;
int width = texture.Info.Width;
int height = texture.Info.Height;
int bytesPerPixel = texture.Info.FormatInfo.BytesPerPixel;
// Determine the SKColorType based on bytes per pixel
SKColorType colorType = bytesPerPixel switch
{
4 => SKColorType.Rgba8888,
3 => SKColorType.Rgb888x,
2 => SKColorType.Rgb565,
_ => SKColorType.Rgba8888
};
// Create SKBitmap from texture data
var imageInfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
using var bitmap = new SKBitmap(imageInfo);
// Copy texture data to bitmap
unsafe
{
fixed (byte* dataPtr = data)
{
bitmap.SetPixels((IntPtr)dataPtr);
}
}
// Create canvas for drawing overlays
using var canvas = new SKCanvas(bitmap);
// Flip Y-axis if the game/texture requires it
// Some games have textures that are already flipped, while others need flipping
if (crop.FlipY)
{
canvas.Scale(1, -1);
canvas.Translate(0, -height);
}
// Render all overlays
_context.OverlayManager.Render(canvas);
// Copy modified bitmap data back to texture data array
var pixels = bitmap.Bytes;
if (pixels.Length <= data.Length)
{
Array.Copy(pixels, data, pixels.Length);
}
// Upload modified data back to texture
var memoryOwner = MemoryOwner<byte>.Rent(data.Length);
data.CopyTo(memoryOwner.Span);
texture.HostTexture.SetData(memoryOwner); // SetData will dispose the MemoryOwner
}
catch (Exception ex)
{
// Silently fail if overlay rendering doesn't work
Logger.Error?.Print(LogClass.Gpu, $"Overlay rendering failed: {ex.Message}");
}
}
/// <summary>
/// Indicate that a frame on the queue is ready to be acquired.
/// </summary>

View File

@ -874,57 +874,42 @@ namespace Ryujinx.Graphics.Vulkan
public unsafe void ConvertIndexBuffer(VulkanRenderer gd,
CommandBufferScoped cbs,
BufferHolder src,
BufferHolder dst,
BufferHolder srcIndexBuffer,
BufferHolder dstIndexBuffer,
IndexBufferPattern pattern,
int indexSize,
int srcOffset,
int indexCount)
{
// TODO: Support conversion with primitive restart enabled.
// TODO: Convert with a compute shader?
int primitiveCount = pattern.GetPrimitiveCount(indexCount);
int convertedCount = pattern.GetConvertedCount(indexCount);
int outputIndexSize = 4;
Buffer srcBuffer = src.GetBuffer().Get(cbs, srcOffset, indexCount * indexSize).Value;
Buffer dstBuffer = dst.GetBuffer().Get(cbs, 0, convertedCount * outputIndexSize).Value;
Buffer dstBuffer = dstIndexBuffer.GetBuffer().Get(cbs, 0, convertedCount * outputIndexSize).Value;
gd.Api.CmdFillBuffer(cbs.CommandBuffer, dstBuffer, 0, Vk.WholeSize, 0);
const int ParamsBufferSize = 16 * sizeof(int);
List<BufferCopy> bufferCopy = [];
int outputOffset = 0;
Span<int> shaderParams = stackalloc int[ParamsBufferSize / sizeof(int)];
// Try to merge copies of adjacent indices to reduce copy count.
int sequenceStart = 0;
int sequenceLength = 0;
shaderParams[8] = pattern.PrimitiveVertices;
shaderParams[9] = pattern.PrimitiveVerticesOut;
shaderParams[10] = indexSize;
shaderParams[11] = outputIndexSize;
shaderParams[12] = pattern.BaseIndex;
shaderParams[13] = pattern.IndexStride;
shaderParams[14] = srcOffset;
shaderParams[15] = primitiveCount;
foreach (int index in pattern.GetIndexMapping(indexCount))
{
if (sequenceLength > 0)
{
if (index == sequenceStart + sequenceLength && indexSize == outputIndexSize)
{
sequenceLength++;
continue;
}
pattern.OffsetIndex.CopyTo(shaderParams[..pattern.OffsetIndex.Length]);
// Commit the copy so far.
bufferCopy.Add(new BufferCopy((ulong)(srcOffset + sequenceStart * indexSize), (ulong)outputOffset, (ulong)(indexSize * sequenceLength)));
outputOffset += outputIndexSize * sequenceLength;
}
using var patternScoped = gd.BufferManager.ReserveOrCreate(gd, cbs, ParamsBufferSize);
var patternBuffer = patternScoped.Holder;
sequenceStart = index;
sequenceLength = 1;
}
patternBuffer.SetDataUnchecked<int>(patternScoped.Offset, shaderParams);
if (sequenceLength > 0)
{
// Commit final pending copy.
bufferCopy.Add(new BufferCopy((ulong)(srcOffset + sequenceStart * indexSize), (ulong)outputOffset, (ulong)(indexSize * sequenceLength)));
}
BufferCopy[] bufferCopyArray = bufferCopy.ToArray();
_pipeline.SetCommandBuffer(cbs);
BufferHolder.InsertBufferBarrier(
gd,
@ -937,10 +922,11 @@ namespace Ryujinx.Graphics.Vulkan
0,
convertedCount * outputIndexSize);
fixed (BufferCopy* pBufferCopy = bufferCopyArray)
{
gd.Api.CmdCopyBuffer(cbs.CommandBuffer, srcBuffer, dstBuffer, (uint)bufferCopyArray.Length, pBufferCopy);
}
_pipeline.SetUniformBuffers([new BufferAssignment(0, new BufferRange(patternScoped.Handle, patternScoped.Offset, ParamsBufferSize))]);
_pipeline.SetStorageBuffers(1, new[] { srcIndexBuffer.GetBuffer(), dstIndexBuffer.GetBuffer() });
_pipeline.SetProgram(_programConvertIndexBuffer);
_pipeline.DispatchCompute(BitUtils.DivRoundUp(primitiveCount, 16), 1, 1);
BufferHolder.InsertBufferBarrier(
gd,
@ -952,6 +938,8 @@ namespace Ryujinx.Graphics.Vulkan
PipelineStageFlags.AllCommandsBit,
0,
convertedCount * outputIndexSize);
_pipeline.Finish(gd, cbs);
}
public void CopyIncompatibleFormats(

View File

@ -47,28 +47,6 @@ namespace Ryujinx.Graphics.Vulkan
return primitiveCount * OffsetIndex.Length;
}
public IEnumerable<int> GetIndexMapping(int indexCount)
{
int primitiveCount = GetPrimitiveCount(indexCount);
int index = BaseIndex;
for (int i = 0; i < primitiveCount; i++)
{
if (RepeatStart)
{
// Used for triangle fan
yield return 0;
}
for (int j = RepeatStart ? 1 : 0; j < OffsetIndex.Length; j++)
{
yield return index + OffsetIndex[j];
}
index += IndexStride;
}
}
public BufferHandle GetRepeatingBuffer(int vertexCount, out int indexCount)
{
int primitiveCount = GetPrimitiveCount(vertexCount);

View File

@ -377,7 +377,7 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
bool cursorVisible = false;
if (state.CursorBegin != state.CursorEnd)
if (state.CursorBegin != state.CursorEnd && state.CursorEnd <= state.InputText.Length)
{
Debug.Assert(state.InputText.Length > 0);

View File

@ -21,6 +21,21 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.Success;
}
[CommandCmif(3)] // 20.0.0+
// CreateLibraryAppletEx(u32, u32, u64) -> object<nn::am::service::ILibraryAppletAccessor>
public ResultCode CreateLibraryAppletEx(ServiceCtx context)
{
AppletId appletId = (AppletId)context.RequestData.ReadInt32();
_ = context.RequestData.ReadInt32(); // libraryAppletMode
_ = context.RequestData.ReadUInt64(); // threadId
MakeObject(context, new ILibraryAppletAccessor(appletId, context.Device.System));
return ResultCode.Success;
}
[CommandCmif(10)]
// CreateStorage(u64) -> object<nn::am::service::IStorage>
public ResultCode CreateStorage(ServiceCtx context)

View File

@ -885,7 +885,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
// F_SETFL
else if (cmd == 0x4)
{
socket.Blocking = (arg & 0x800) != 0;
socket.Blocking = (arg & 0x800) == 0;
result = 0;
}
else

View File

@ -3,6 +3,7 @@ using Ryujinx.Audio.Integration;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
@ -65,6 +66,12 @@ namespace Ryujinx.HLE
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal IHostUIHandler HostUIHandler { get; private set; }
/// <summary>
/// The overlay manager to use for all overlay operations.
/// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal IOverlayManager OverlayManager { get; private set; }
/// <summary>
/// Control the memory configuration used by the emulation context.
/// </summary>
@ -262,7 +269,8 @@ namespace Ryujinx.HLE
UserChannelPersistence userChannelPersistence,
IRenderer gpuRenderer,
IHardwareDeviceDriver audioDeviceDriver,
IHostUIHandler hostUIHandler
IHostUIHandler hostUIHandler,
IOverlayManager overlayManager
)
{
VirtualFileSystem = virtualFileSystem;
@ -273,6 +281,7 @@ namespace Ryujinx.HLE
GpuRenderer = gpuRenderer;
AudioDeviceDriver = audioDeviceDriver;
HostUIHandler = hostUIHandler;
OverlayManager = overlayManager;
return this;
}
}

View File

@ -71,7 +71,7 @@ namespace Ryujinx.HLE
DirtyHacks = new DirtyHacks(Configuration.Hacks);
AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver);
Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags);
Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks);
Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks, Configuration.OverlayManager);
System = new HOS.Horizon(this);
Statistics = new PerformanceStatistics(this);
Hid = new Hid(this, System.HidStorage);

View File

@ -372,6 +372,12 @@
<Setter Property="BorderThickness"
Value="2"/>
</Style>
<Style Selector="Border.listbox-item-style">
<Setter Property="Padding" Value="10" />
<Setter Property="Margin" Value="5,0,5,0" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Background" Value="{DynamicResource AppListBackgroundColor}" />
</Style>
<Style Selector="ListBox ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background"
Value="{DynamicResource AppListBackgroundColor}" />

View File

@ -15,5 +15,14 @@ namespace Ryujinx.Ava.Common
CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement,
TurboMode,
CycleInputDevicePlayer1,
CycleInputDevicePlayer2,
CycleInputDevicePlayer3,
CycleInputDevicePlayer4,
CycleInputDevicePlayer5,
CycleInputDevicePlayer6,
CycleInputDevicePlayer7,
CycleInputDevicePlayer8,
CycleInputDeviceHandheld,
}
}

View File

@ -1,3 +1,4 @@
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Projektanker.Icons.Avalonia;
using Ryujinx.Ava.Common.Locale;
@ -18,11 +19,19 @@ namespace Ryujinx.Ava.Common.Markup
internal class LocaleExtension(LocaleKeys key) : BasicMarkupExtension<string>
{
public IValueConverter Converter { get; set; }
public override string Name => "Translation";
protected override string Value => LocaleManager.Instance[key];
protected override void ConfigureBindingExtension(CompiledBindingExtension bindingExtension)
=> bindingExtension.Source = LocaleManager.Instance;
{
bindingExtension.Source = LocaleManager.Instance;
if (Converter != null)
{
bindingExtension.Converter = Converter;
}
}
}
internal class WindowTitleExtension(LocaleKeys key, bool includeVersion) : BasicMarkupExtension<string>

View File

@ -1,9 +0,0 @@
namespace Ryujinx.Ava.Common.Models.Github
{
public class GithubReleaseAssetJsonResponse
{
public string Name { get; set; }
public string State { get; set; }
public string BrowserDownloadUrl { get; set; }
}
}

View File

@ -1,12 +0,0 @@
using System.Collections.Generic;
namespace Ryujinx.Ava.Common.Models.Github
{
public class GithubReleasesJsonResponse
{
public string Name { get; set; }
public string TagName { get; set; }
public List<GithubReleaseAssetJsonResponse> Assets { get; set; }
}
}

View File

@ -1,7 +0,0 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Common.Models.Github
{
[JsonSerializable(typeof(GithubReleasesJsonResponse), GenerationMode = JsonSourceGenerationMode.Metadata)]
public partial class GithubReleasesJsonSerializerContext : JsonSerializerContext;
}

View File

@ -17,6 +17,7 @@ using Ryujinx.Graphics.OpenGL;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE;
using Ryujinx.Input;
using Ryujinx.UI.Overlay;
using Silk.NET.Vulkan;
using System;
using System.IO;
@ -348,7 +349,8 @@ namespace Ryujinx.Headless
_userChannelPersistence,
renderer.TryMakeThreaded(options.BackendThreading),
new SDL2HardwareDeviceDriver(),
window
window,
new OverlayManager()
)
);
}

View File

@ -35,6 +35,7 @@ namespace Ryujinx.Ava
public static string Version { get; private set; }
public static string ConfigurationPath { get; private set; }
public static string GlobalConfigurationPath { get; private set; }
public static bool UseExtraConfig { get; set; }
public static bool PreviewerDetached { get; private set; }
public static bool UseHardwareAcceleration { get; private set; }
public static string BackendThreadingArg { get; private set; }
@ -159,7 +160,8 @@ namespace Ryujinx.Ava
}
}
public static string GetDirGameUserConfig(string gameId, bool rememberGlobalDir = false, bool changeFolderForGame = false)
public static string GetDirGameUserConfig(string gameId, bool changeFolderForGame = false)
{
if (string.IsNullOrEmpty(gameId))
{
@ -168,15 +170,10 @@ namespace Ryujinx.Ava
string gameDir = Path.Combine(AppDataManager.GamesDirPath, gameId, ReleaseInformation.ConfigName);
// Should load with the game if there is a custom setting for the game
if (rememberGlobalDir)
{
GlobalConfigurationPath = ConfigurationPath;
}
if (changeFolderForGame)
{
ConfigurationPath = gameDir;
UseExtraConfig = true;
}
return gameDir;
@ -184,8 +181,6 @@ namespace Ryujinx.Ava
public static void ReloadConfig()
{
//It is necessary that when a user setting appears, the global setting remains available
GlobalConfigurationPath = null;
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
@ -225,6 +220,12 @@ namespace Ryujinx.Ava
}
}
// When you first load the program, copy to remember the path for the global configuration
if (GlobalConfigurationPath == null)
{
GlobalConfigurationPath = ConfigurationPath;
}
UseHardwareAcceleration = ConfigurationState.Instance.EnableHardwareAcceleration;
// Check if graphics backend was overridden

View File

@ -65,6 +65,8 @@
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" />
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'win-arm64'" />
<PackageReference Include="Ryujinx.UpdateClient" />
<PackageReference Include="Ryujinx.Systems.Update.Common" />
<PackageReference Include="securifybv.ShellLink" />
<PackageReference Include="Sep" />
<PackageReference Include="Silk.NET.Vulkan" />

View File

@ -33,6 +33,7 @@ using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu;
using Ryujinx.UI.Overlay;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE.FileSystem;
@ -40,6 +41,11 @@ using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
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 System.Linq;
using SkiaSharp;
using SPB.Graphics.Vulkan;
using System;
@ -75,6 +81,7 @@ namespace Ryujinx.Ava.Systems
private readonly long _ticksPerFrame;
private readonly Stopwatch _chrono;
private readonly Stopwatch _playTimer;
private long _ticks;
private readonly AccountManager _accountManager;
@ -123,6 +130,7 @@ namespace Ryujinx.Ava.Systems
private readonly bool _isFirmwareTitle;
private readonly Lock _lockObject = new();
private ControllerOverlay _controllerOverlay;
public event EventHandler AppExit;
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
@ -175,6 +183,7 @@ namespace Ryujinx.Ava.Systems
_chrono = new Stopwatch();
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_playTimer = new Stopwatch();
if (ApplicationPath.StartsWith("@SystemContent"))
{
@ -461,7 +470,15 @@ namespace Ryujinx.Ava.Systems
DisplaySleep.Prevent();
if (ConfigurationState.Instance.System.UseInputGlobalConfig.Value && Program.UseExtraConfig)
{
NpadManager.Initialize(Device, ConfigurationState.InstanceExtra.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
}
else
{
NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
}
TouchScreenManager.Initialize(Device);
_viewModel.IsGameRunning = true;
@ -557,6 +574,7 @@ namespace Ryujinx.Ava.Systems
public void Stop()
{
_isActive = false;
_playTimer.Stop();
}
private void Exit()
@ -608,7 +626,7 @@ namespace Ryujinx.Ava.Systems
private void Dispose()
{
if (Device.Processes != null)
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText);
MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed);
ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState;
ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState;
@ -627,6 +645,7 @@ namespace Ryujinx.Ava.Systems
_gpuCancellationTokenSource.Dispose();
_chrono.Stop();
_playTimer.Stop();
}
public void DisposeGpu()
@ -860,6 +879,7 @@ namespace Ryujinx.Ava.Systems
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText,
appMetadata => appMetadata.UpdatePreGame()
);
_playTimer.Start();
return true;
}
@ -869,6 +889,7 @@ namespace Ryujinx.Ava.Systems
Device?.System.TogglePauseEmulation(false);
_viewModel.IsPaused = false;
_playTimer.Start();
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI);
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
}
@ -878,6 +899,7 @@ namespace Ryujinx.Ava.Systems
Device?.System.TogglePauseEmulation(true);
_viewModel.IsPaused = true;
_playTimer.Stop();
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]);
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
}
@ -909,9 +931,13 @@ namespace Ryujinx.Ava.Systems
_userChannelPersistence,
renderer.TryMakeThreaded(ConfigurationState.Instance.Graphics.BackendThreading),
InitializeAudio(),
_viewModel.UiHandler
_viewModel.UiHandler,
new OverlayManager()
)
);
_controllerOverlay = new ControllerOverlay();
Device.Gpu.Window.AddOverlay(_controllerOverlay);
}
private static IHardwareDeviceDriver InitializeAudio()
@ -1144,6 +1170,24 @@ namespace Ryujinx.Ava.Systems
_dialogShown = true;
// The hard-coded hotkey mapped to exit is Escape, but it's also the same key
// that causes the dialog we launch to close (without doing anything). In release
// mode, a race is observed that between ShowExitPrompt() appearing on KeyDown
// and the ContentDialog we create seeing the key state before KeyUp. Merely waiting
// for the key to no longer be pressed appears to be insufficient.
// NB: Using _keyboardInterface.IsPressed(Key.Escape) does not currently work.
if (OperatingSystem.IsWindows())
{
while (GetAsyncKeyState(0x1B) != 0)
{
await Task.Delay(100);
}
}
else
{
await Task.Delay(250);
}
shouldExit = await ContentDialogHelper.CreateStopEmulationDialog();
_dialogShown = false;
@ -1300,6 +1344,33 @@ namespace Ryujinx.Ava.Systems
_viewModel.Volume = Device.GetVolume();
break;
case KeyboardHotkeyState.CycleInputDevicePlayer1:
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player1);
break;
case KeyboardHotkeyState.CycleInputDevicePlayer2:
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player2);
break;
case KeyboardHotkeyState.CycleInputDevicePlayer3:
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player3);
break;
case KeyboardHotkeyState.CycleInputDevicePlayer4:
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player4);
break;
case KeyboardHotkeyState.CycleInputDevicePlayer5:
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player5);
break;
case KeyboardHotkeyState.CycleInputDevicePlayer6:
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player6);
break;
case KeyboardHotkeyState.CycleInputDevicePlayer7:
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player7);
break;
case KeyboardHotkeyState.CycleInputDevicePlayer8:
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Player8);
break;
case KeyboardHotkeyState.CycleInputDeviceHandheld:
CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex.Handheld);
break;
case KeyboardHotkeyState.None:
(_keyboardInterface as AvaloniaKeyboard).Clear();
break;
@ -1333,6 +1404,118 @@ namespace Ryujinx.Ava.Systems
return true;
}
private void CycleInputDevice(HLE.HOS.Services.Hid.PlayerIndex playerIndex)
{
// Get current input configuration
List<InputConfig> currentConfig = new(ConfigurationState.Instance.Hid.InputConfig.Value);
// Find current configuration for this player
InputConfig playerConfig = currentConfig.FirstOrDefault(x => x.PlayerIndex == (PlayerIndex)playerIndex);
// Get available devices from InputManager
List<(DeviceType Type, string Id, string Name)> availableDevices = [];
// Add disabled option
availableDevices.Add((DeviceType.None, "disabled", "Disabled"));
// Add keyboard devices
foreach (string id in _inputManager.KeyboardDriver.GamepadsIds)
{
using var gamepad = _inputManager.KeyboardDriver.GetGamepad(id);
if (gamepad != null)
{
availableDevices.Add((DeviceType.Keyboard, id, gamepad.Name));
}
}
// Add controller devices
int controllerNumber = 0;
foreach (string id in _inputManager.GamepadDriver.GamepadsIds)
{
using var gamepad = _inputManager.GamepadDriver.GetGamepad(id);
if (gamepad != null)
{
string name = $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)} ({controllerNumber++})";
availableDevices.Add((DeviceType.Controller, id, name));
}
}
// Find current device index
int currentIndex = 0;
if (playerConfig != null)
{
DeviceType currentType = DeviceType.None;
if (playerConfig is StandardKeyboardInputConfig)
currentType = DeviceType.Keyboard;
else if (playerConfig is StandardControllerInputConfig)
currentType = DeviceType.Controller;
currentIndex = availableDevices.FindIndex(x => x.Type == currentType && x.Id == playerConfig.Id);
if (currentIndex == -1) currentIndex = 0;
}
// Cycle to next device
int nextIndex = (currentIndex + 1) % availableDevices.Count;
var nextDevice = availableDevices[nextIndex];
// Remove existing configuration for this player
currentConfig.RemoveAll(x => x.PlayerIndex == (PlayerIndex)playerIndex);
// Add new configuration if not disabled
if (nextDevice.Type != DeviceType.None)
{
InputConfig newConfig = CreateDefaultInputConfig(nextDevice, (PlayerIndex)playerIndex);
if (newConfig != null)
{
currentConfig.Add(newConfig);
}
}
// Apply the new configuration
ConfigurationState.Instance.Hid.InputConfig.Value = currentConfig;
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
// Reload the input system
NpadManager.ReloadConfiguration(currentConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
// Show controller overlay
ShowControllerOverlay(currentConfig, ConfigurationState.Instance.ControllerOverlayInputCycleDuration.Value);
}
private InputConfig CreateDefaultInputConfig((DeviceType Type, string Id, string Name) device, PlayerIndex playerIndex)
{
if (device.Type == DeviceType.Keyboard)
{
return DefaultInputConfigurationProvider.CreateDefaultKeyboardConfig(device.Id, device.Name, playerIndex);
}
else if (device.Type == DeviceType.Controller)
{
bool isNintendoStyle = DefaultInputConfigurationProvider.IsNintendoStyleController(device.Name);
return DefaultInputConfigurationProvider.CreateDefaultControllerConfig(device.Id, device.Name, playerIndex, isNintendoStyle);
}
return null;
}
public void ShowControllerOverlay(List<InputConfig> inputConfigs, int duration)
{
try
{
if (_controllerOverlay != null)
{
_controllerOverlay.ShowControllerBindings(inputConfigs, duration);
}
else
{
Logger.Warning?.Print(LogClass.Application, "AppHost: Cannot show overlay - ControllerOverlay is null");
}
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.Application, $"Failed to show controller overlay: {ex.Message}");
}
}
private KeyboardHotkeyState GetHotkeyState()
{
KeyboardHotkeyState state = KeyboardHotkeyState.None;
@ -1385,6 +1568,42 @@ namespace Ryujinx.Ava.Systems
{
state = KeyboardHotkeyState.TurboMode;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer1))
{
state = KeyboardHotkeyState.CycleInputDevicePlayer1;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer2))
{
state = KeyboardHotkeyState.CycleInputDevicePlayer2;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer3))
{
state = KeyboardHotkeyState.CycleInputDevicePlayer3;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer4))
{
state = KeyboardHotkeyState.CycleInputDevicePlayer4;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer5))
{
state = KeyboardHotkeyState.CycleInputDevicePlayer5;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer6))
{
state = KeyboardHotkeyState.CycleInputDevicePlayer6;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer7))
{
state = KeyboardHotkeyState.CycleInputDevicePlayer7;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDevicePlayer8))
{
state = KeyboardHotkeyState.CycleInputDevicePlayer8;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CycleInputDeviceHandheld))
{
state = KeyboardHotkeyState.CycleInputDeviceHandheld;
}
return state;
}

View File

@ -556,7 +556,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
data.Favorite = appMetadata.Favorite;
data.TimePlayed = appMetadata.TimePlayed;
data.LastPlayed = appMetadata.LastPlayed;
data.HasIndependentConfiguration = File.Exists(Program.GetDirGameUserConfig(data.IdBaseString, false, false)); // Just check user config
data.HasIndependentConfiguration = File.Exists(Program.GetDirGameUserConfig(data.IdBaseString)); // Just check user config
}
data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper();

View File

@ -33,19 +33,11 @@ namespace Ryujinx.Ava.Systems.AppLibrary
/// <summary>
/// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends.
/// </summary>
public void UpdatePostGame()
/// <param name="playTime">The active gameplay time this past session.</param>
public void UpdatePostGame(TimeSpan playTime)
{
DateTime? prevLastPlayed = LastPlayed;
UpdatePreGame();
if (!prevLastPlayed.HasValue)
{
return;
}
TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value;
double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds;
TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
TimePlayed += playTime;
}
}
}

View File

@ -15,7 +15,7 @@ namespace Ryujinx.Ava.Systems.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 69;
public const int CurrentVersion = 72;
/// <summary>
/// Version of the configuration file format
@ -152,6 +152,11 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public bool MatchSystemTime { get; set; }
/// <summary>
/// Enable or disable use global input config (Independent from controllers binding)
/// </summary>
public bool UseInputGlobalConfig { get; set; }
/// <summary>
/// Enables or disables Docked Mode
/// </summary>
@ -212,6 +217,16 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public HideCursorMode HideCursor { get; set; }
/// <summary>
/// Duration to show controller overlay when game starts (seconds, 0 = disabled)
/// </summary>
public int ControllerOverlayGameStartDuration { get; set; }
/// <summary>
/// Duration to show controller overlay when input is cycled (seconds, 0 = disabled)
/// </summary>
public int ControllerOverlayInputCycleDuration { get; set; }
/// <summary>
/// Enables or disables Vertical Sync
/// </summary>

View File

@ -53,6 +53,8 @@ namespace Ryujinx.Ava.Systems.Configuration
ShowOldUI.Value = shouldLoadFromFile ? cff.ShowTitleBar : ShowOldUI.Value; // Get from global config only
EnableHardwareAcceleration.Value = shouldLoadFromFile ? cff.EnableHardwareAcceleration : EnableHardwareAcceleration.Value; // Get from global config only
HideCursor.Value = cff.HideCursor;
ControllerOverlayGameStartDuration.Value = cff.ControllerOverlayGameStartDuration;
ControllerOverlayInputCycleDuration.Value = cff.ControllerOverlayInputCycleDuration;
Logger.EnableFileLog.Value = cff.EnableFileLog;
Logger.EnableDebug.Value = cff.LoggingEnableDebug;
@ -90,6 +92,7 @@ namespace Ryujinx.Ava.Systems.Configuration
System.TimeZone.Value = cff.SystemTimeZone;
System.SystemTimeOffset.Value = shouldLoadFromFile ? cff.SystemTimeOffset : System.SystemTimeOffset.Value; // Get from global config only
System.MatchSystemTime.Value = shouldLoadFromFile ? cff.MatchSystemTime : System.MatchSystemTime.Value; // Get from global config only
System.UseInputGlobalConfig.Value = cff.UseInputGlobalConfig;
System.EnableDockedMode.Value = cff.DockedMode;
System.EnablePtc.Value = cff.EnablePtc;
System.EnableLowPowerPtc.Value = cff.EnableLowPowerPtc;
@ -146,7 +149,7 @@ namespace Ryujinx.Ava.Systems.Configuration
Hid.EnableMouse.Value = cff.EnableMouse;
Hid.DisableInputWhenOutOfFocus.Value = shouldLoadFromFile ? cff.DisableInputWhenOutOfFocus : Hid.DisableInputWhenOutOfFocus.Value; // Get from global config only
Hid.Hotkeys.Value = shouldLoadFromFile ? cff.Hotkeys : Hid.Hotkeys.Value; // Get from global config only
Hid.InputConfig.Value = cff.InputConfig ?? [];
Hid.InputConfig.Value = cff.InputConfig ?? [] ;
Hid.RainbowSpeed.Value = cff.RainbowSpeed;
Multiplayer.LanInterfaceId.Value = cff.MultiplayerLanInterfaceId;
@ -478,7 +481,40 @@ namespace Ryujinx.Ava.Systems.Configuration
};
}
),
(69, static cff => cff.SkipUserProfiles = false)
(69, static cff => cff.SkipUserProfiles = false),
(71, static cff =>
{
cff.ControllerOverlayGameStartDuration = 3;
cff.ControllerOverlayInputCycleDuration = 2;
}),
(72, static cff =>
{
cff.Hotkeys = new KeyboardHotkeys
{
ToggleVSyncMode = cff.Hotkeys.ToggleVSyncMode,
Screenshot = cff.Hotkeys.Screenshot,
ShowUI = cff.Hotkeys.ShowUI,
Pause = cff.Hotkeys.Pause,
ToggleMute = cff.Hotkeys.ToggleMute,
ResScaleUp = cff.Hotkeys.ResScaleUp,
ResScaleDown = cff.Hotkeys.ResScaleDown,
VolumeUp = cff.Hotkeys.VolumeUp,
VolumeDown = cff.Hotkeys.VolumeDown,
CustomVSyncIntervalIncrement = cff.Hotkeys.CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement = cff.Hotkeys.CustomVSyncIntervalDecrement,
TurboMode = cff.Hotkeys.TurboMode,
TurboModeWhileHeld = cff.Hotkeys.TurboModeWhileHeld,
CycleInputDevicePlayer1 = Key.Unbound,
CycleInputDevicePlayer2 = Key.Unbound,
CycleInputDevicePlayer3 = Key.Unbound,
CycleInputDevicePlayer4 = Key.Unbound,
CycleInputDevicePlayer5 = Key.Unbound,
CycleInputDevicePlayer6 = Key.Unbound,
CycleInputDevicePlayer7 = Key.Unbound,
CycleInputDevicePlayer8 = Key.Unbound,
CycleInputDeviceHandheld = Key.Unbound
};
})
);
}
}

View File

@ -326,6 +326,12 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public ReactiveObject<bool> MatchSystemTime { get; private set; }
/// <summary>
/// Enable or disable use global input config (Independent from controllers binding)
/// </summary>
public ReactiveObject<bool> UseInputGlobalConfig { get; private set; }
/// <summary>
/// Enables or disables Docked Mode
/// </summary>
@ -417,6 +423,8 @@ namespace Ryujinx.Ava.Systems.Configuration
SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset));
MatchSystemTime = new ReactiveObject<bool>();
MatchSystemTime.LogChangesToValue(nameof(MatchSystemTime));
UseInputGlobalConfig = new ReactiveObject<bool>();
UseInputGlobalConfig.LogChangesToValue(nameof(UseInputGlobalConfig));
EnableDockedMode = new ReactiveObject<bool>();
EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode));
EnablePtc = new ReactiveObject<bool>();
@ -761,6 +769,8 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public static ConfigurationState Instance { get; private set; }
public static ConfigurationState InstanceExtra{ get; private set; }
/// <summary>
/// The UI section
/// </summary>
@ -836,6 +846,16 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public ReactiveObject<HideCursorMode> HideCursor { get; private set; }
/// <summary>
/// Duration to show controller overlay when game starts (seconds, 0 = disabled)
/// </summary>
public ReactiveObject<int> ControllerOverlayGameStartDuration { get; private set; }
/// <summary>
/// Duration to show controller overlay when input is cycled (seconds, 0 = disabled)
/// </summary>
public ReactiveObject<int> ControllerOverlayInputCycleDuration { get; private set; }
private ConfigurationState()
{
UI = new UISection();
@ -853,6 +873,8 @@ namespace Ryujinx.Ava.Systems.Configuration
RememberWindowState = new ReactiveObject<bool>();
ShowOldUI = new ReactiveObject<bool>();
EnableHardwareAcceleration = new ReactiveObject<bool>();
ControllerOverlayGameStartDuration = new ReactiveObject<int>();
ControllerOverlayInputCycleDuration = new ReactiveObject<int>();
}
public HleConfiguration CreateHleConfiguration() =>

View File

@ -15,12 +15,13 @@ namespace Ryujinx.Ava.Systems.Configuration
{
public static void Initialize()
{
if (Instance != null)
if (Instance != null || InstanceExtra!= null)
{
throw new InvalidOperationException("Configuration is already initialized");
}
Instance = new ConfigurationState();
InstanceExtra= new ConfigurationState();
}
public ConfigurationFileFormat ToFileFormat()
@ -54,6 +55,7 @@ namespace Ryujinx.Ava.Systems.Configuration
SystemTimeZone = System.TimeZone,
SystemTimeOffset = System.SystemTimeOffset,
MatchSystemTime = System.MatchSystemTime,
UseInputGlobalConfig = System.UseInputGlobalConfig,
DockedMode = System.EnableDockedMode,
EnableDiscordIntegration = EnableDiscordIntegration,
UpdateCheckerType = UpdateCheckerType,
@ -63,6 +65,8 @@ namespace Ryujinx.Ava.Systems.Configuration
ShowTitleBar = ShowOldUI,
EnableHardwareAcceleration = EnableHardwareAcceleration,
HideCursor = HideCursor,
ControllerOverlayGameStartDuration = ControllerOverlayGameStartDuration,
ControllerOverlayInputCycleDuration = ControllerOverlayInputCycleDuration,
VSyncMode = Graphics.VSyncMode,
EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval,
CustomVSyncInterval = Graphics.CustomVSyncInterval,
@ -178,6 +182,7 @@ namespace Ryujinx.Ava.Systems.Configuration
System.Region.Value = Region.USA;
System.TimeZone.Value = "UTC";
System.SystemTimeOffset.Value = 0;
System.UseInputGlobalConfig.Value = false;
System.EnableDockedMode.Value = true;
EnableDiscordIntegration.Value = true;
UpdateCheckerType.Value = UpdaterType.PromptAtStartup;
@ -187,6 +192,8 @@ namespace Ryujinx.Ava.Systems.Configuration
ShowOldUI.Value = !OperatingSystem.IsWindows();
EnableHardwareAcceleration.Value = true;
HideCursor.Value = HideCursorMode.OnIdle;
ControllerOverlayGameStartDuration.Value = 3;
ControllerOverlayInputCycleDuration.Value = 2;
Graphics.VSyncMode.Value = VSyncMode.Switch;
Graphics.CustomVSyncInterval.Value = 120;
Graphics.EnableCustomVSyncInterval.Value = false;
@ -266,7 +273,16 @@ namespace Ryujinx.Ava.Systems.Configuration
CustomVSyncIntervalIncrement = Key.Unbound,
CustomVSyncIntervalDecrement = Key.Unbound,
TurboMode = Key.Unbound,
TurboModeWhileHeld = false
TurboModeWhileHeld = false,
CycleInputDevicePlayer1 = Key.Unbound,
CycleInputDevicePlayer2 = Key.Unbound,
CycleInputDevicePlayer3 = Key.Unbound,
CycleInputDevicePlayer4 = Key.Unbound,
CycleInputDevicePlayer5 = Key.Unbound,
CycleInputDevicePlayer6 = Key.Unbound,
CycleInputDevicePlayer7 = Key.Unbound,
CycleInputDevicePlayer8 = Key.Unbound,
CycleInputDeviceHandheld = Key.Unbound
};
Hid.RainbowSpeed.Value = 1f;
Hid.InputConfig.Value =

View File

@ -0,0 +1,107 @@
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Systems.Update.Client;
using Ryujinx.Systems.Update.Common;
using System;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Systems
{
internal static partial class Updater
{
private static VersionResponse _versionResponse;
private static UpdateClient CreateUpdateClient()
=> UpdateClient.Builder()
.WithServerEndpoint("https://update.ryujinx.app") // This is the default, and doesn't need to be provided; it's here for transparency.
.WithLogger((format, args, caller) =>
Logger.Info?.Print(
LogClass.Application,
args.Length is 0 ? format : format.Format(args),
caller: caller)
);
public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)
{
if (!Version.TryParse(Program.Version, out Version currentVersion))
{
Logger.Error?.Print(LogClass.Application,
$"Failed to convert the current {RyujinxApp.FullAppName} version!");
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
_running = false;
return default;
}
using UpdateClient updateClient = CreateUpdateClient();
try
{
_versionResponse = await updateClient.QueryLatestAsync(ReleaseInformation.IsCanaryBuild
? ReleaseChannel.Canary
: ReleaseChannel.Stable);
}
catch (Exception e)
{
Logger.Error?.Print(LogClass.Application, $"An error occurred when requesting for updates ({e.GetType().AsFullNamePrettyString()}): {e.Message}");
_running = false;
return default;
}
if (_versionResponse == null)
{
// logging is done via the UpdateClient library
_running = false;
return default;
}
// If build URL not found, assume no new update is available.
if (_versionResponse.ArtifactUrl is null or "")
{
if (showVersionUpToDate)
{
UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
string.Empty);
if (userResult is UserResult.Ok)
{
OpenHelper.OpenUrl(_versionResponse.ReleaseUrlFormat.Format(currentVersion));
}
}
Logger.Info?.Print(LogClass.Application, "Up to date.");
_running = false;
return default;
}
if (!Version.TryParse(_versionResponse.Version, out Version newVersion))
{
Logger.Error?.Print(LogClass.Application,
$"Failed to convert the received {RyujinxApp.FullAppName} version from the update server!");
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedServerMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
_running = false;
return default;
}
return (currentVersion, newVersion);
}
}
}

View File

@ -5,13 +5,11 @@ using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models.Github;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@ -29,147 +27,19 @@ using System.Threading.Tasks;
namespace Ryujinx.Ava.Systems
{
internal static class Updater
internal static partial class Updater
{
private static ReleaseChannels.Channel? _currentReleaseChannel;
private const string GitHubApiUrl = "https://api.github.com";
private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
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 string _buildVer;
private static readonly string _platformExt = BuildPlatformExtension();
private static string _buildUrl;
private static long _buildSize;
private static bool _updateSuccessful;
private static bool _running;
private static readonly string[] _windowsDependencyDirs = [];
public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)
{
if (!Version.TryParse(Program.Version, out Version currentVersion))
{
Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!");
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
_running = false;
return default;
}
Logger.Info?.Print(LogClass.Application, "Checking for updates.");
// Get latest version number from GitHub API
try
{
using HttpClient jsonClient = ConstructHttpClient();
if (_currentReleaseChannel == null)
{
ReleaseChannels releaseChannels = await ReleaseInformation.GetReleaseChannelsAsync(jsonClient);
_currentReleaseChannel = ReleaseInformation.IsCanaryBuild
? releaseChannels.Canary
: releaseChannels.Stable;
}
string fetchedJson = await jsonClient.GetStringAsync(_currentReleaseChannel.Value.GetLatestReleaseApiUrl());
GithubReleasesJsonResponse fetched = JsonHelper.Deserialize(fetchedJson, _serializerContext.GithubReleasesJsonResponse);
_buildVer = fetched.TagName;
foreach (GithubReleaseAssetJsonResponse asset in fetched.Assets)
{
if (asset.Name.StartsWith("ryujinx") && asset.Name.EndsWith(_platformExt))
{
_buildUrl = asset.BrowserDownloadUrl;
if (asset.State != "uploaded")
{
if (showVersionUpToDate)
{
UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
string.Empty);
if (userResult is UserResult.Ok)
{
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion, _currentReleaseChannel.Value));
}
}
Logger.Info?.Print(LogClass.Application, "Up to date.");
_running = false;
return default;
}
break;
}
}
// If build not done, assume no new update are available.
if (_buildUrl is null)
{
if (showVersionUpToDate)
{
UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage],
string.Empty);
if (userResult is UserResult.Ok)
{
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion, _currentReleaseChannel.Value));
}
}
Logger.Info?.Print(LogClass.Application, "Up to date.");
_running = false;
return default;
}
}
catch (Exception exception)
{
Logger.Error?.Print(LogClass.Application, exception.Message);
await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterFailedToGetVersionMessage]);
_running = false;
return default;
}
if (!Version.TryParse(_buildVer, out Version newVersion))
{
Logger.Error?.Print(LogClass.Application, $"Failed to convert the received {RyujinxApp.FullAppName} version from GitHub!");
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
_running = false;
return default;
}
return (currentVersion, newVersion);
}
public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
{
if (_running)
@ -196,7 +66,7 @@ namespace Ryujinx.Ava.Systems
if (userResult is UserResult.Ok)
{
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion, _currentReleaseChannel.Value));
OpenHelper.OpenUrl(_versionResponse.ReleaseUrlFormat.Format(currentVersion));
}
}
@ -213,7 +83,10 @@ namespace Ryujinx.Ava.Systems
{
buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_buildUrl), HttpCompletionOption.ResponseHeadersRead);
// 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;
}
@ -243,11 +116,11 @@ namespace Ryujinx.Ava.Systems
switch (shouldUpdate)
{
case UserResult.Yes:
await UpdateRyujinx(_buildUrl);
await UpdateRyujinx(_versionResponse.ArtifactUrl);
break;
// Secondary button maps to no, which in this case is the show changelog button.
case UserResult.No:
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogUrl(currentVersion, newVersion, _currentReleaseChannel.Value));
OpenHelper.OpenUrl(ReleaseInformation.GetChangelogUrl(currentVersion, newVersion));
goto RequestUserToUpdate;
default:
_running = false;
@ -261,7 +134,7 @@ namespace Ryujinx.Ava.Systems
HttpClient result = new();
// Required by GitHub to interact with APIs.
result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
result.DefaultRequestHeaders.Add("User-Agent", $"Ryujinx-Updater/{ReleaseInformation.Version}");
return result;
}

View File

@ -2,29 +2,104 @@ using Avalonia.Media;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.HLE.UI;
using System;
using System.Globalization;
namespace Ryujinx.Ava.UI.Applet
{
class AvaloniaHostUITheme(MainWindow parent) : IHostUITheme
class AvaloniaHostUITheme : IHostUITheme
{
public string FontFamily { get; } = OperatingSystem.IsWindows() && OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) ? "Segoe UI Variable" : parent.FontFamily.Name;
private readonly MainWindow _parent;
public ThemeColor DefaultBackgroundColor { get; } = BrushToThemeColor(parent.Background);
public ThemeColor DefaultForegroundColor { get; } = BrushToThemeColor(parent.Foreground);
public ThemeColor DefaultBorderColor { get; } = BrushToThemeColor(parent.BorderBrush);
public ThemeColor SelectionBackgroundColor { get; } = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionBrush);
public ThemeColor SelectionForegroundColor { get; } = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionForegroundBrush);
public string FontFamily { get; }
public ThemeColor DefaultBackgroundColor { get; }
public ThemeColor DefaultForegroundColor { get; }
public ThemeColor DefaultBorderColor { get; }
public ThemeColor SelectionBackgroundColor { get; }
public ThemeColor SelectionForegroundColor { get; }
public AvaloniaHostUITheme(MainWindow parent)
{
_parent = parent;
// Initialize font property
FontFamily = GetSystemFontFamily();
// Initialize all properties that depend on parent
DefaultBackgroundColor = BrushToThemeColor(parent.Background);
DefaultForegroundColor = BrushToThemeColor(parent.Foreground);
DefaultBorderColor = BrushToThemeColor(parent.BorderBrush);
SelectionBackgroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionBrush);
SelectionForegroundColor = BrushToThemeColor(parent.ViewControls.SearchBox.SelectionForegroundBrush);
}
private string GetSystemFontFamily()
{
if (OperatingSystem.IsWindows())
{
return GetWindowsFontByLanguage();
}
else if (OperatingSystem.IsMacOS())
{
return GetMacOSFontByLanguage();
}
else // Linux and other platforms
{
return GetLinuxFontByLanguage();
}
}
private string GetWindowsFontByLanguage()
{
var culture = CultureInfo.CurrentUICulture;
string langCode = culture.Name;
return culture.TwoLetterISOLanguageName switch
{
"zh" => langCode == "zh-CN" || langCode == "zh-Hans" || langCode == "zh-SG"
? "Microsoft YaHei UI" // Simplified Chinese
: "Microsoft JhengHei UI", // Traditional Chinese
"ja" => "Yu Gothic UI", // Japanese
"ko" => "Malgun Gothic", // Korean
_ => OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)
? "Segoe UI Variable" // Other languages - Windows 11+
: _parent.FontFamily.Name // Fallback to parent window font
};
}
private string GetMacOSFontByLanguage()
{
return CultureInfo.CurrentUICulture.TwoLetterISOLanguageName switch
{
"zh" => "PingFang SC", // Chinese (both simplified and traditional)
"ja" => "Hiragino Sans", // Japanese
"ko" => "Apple SD Gothic Neo", // Korean
_ => _parent.FontFamily.Name // Fallback to parent window font
};
}
private string GetLinuxFontByLanguage()
{
return CultureInfo.CurrentUICulture.TwoLetterISOLanguageName switch
{
"zh" => "Noto Sans CJK SC", // Chinese
"ja" => "Noto Sans CJK JP", // Japanese
"ko" => "Noto Sans CJK KR", // Korean
_ => _parent.FontFamily.Name // Fallback to parent window font
};
}
private static ThemeColor BrushToThemeColor(IBrush brush)
{
if (brush is SolidColorBrush solidColor)
{
return new ThemeColor((float)solidColor.Color.A / 255,
return new ThemeColor(
(float)solidColor.Color.A / 255,
(float)solidColor.Color.R / 255,
(float)solidColor.Color.G / 255,
(float)solidColor.Color.B / 255);
(float)solidColor.Color.B / 255
);
}
return new ThemeColor();
}
}

View File

@ -0,0 +1,28 @@
using Avalonia.Data.Converters;
using Ryujinx.Ava.Common.Locale;
using System;
using System.Globalization;
namespace Ryujinx.Ava.UI.Helpers
{
internal class PlayerHotkeyLabelConverter : IValueConverter
{
public static readonly PlayerHotkeyLabelConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string playerName && !string.IsNullOrEmpty(playerName))
{
string baseText = LocaleManager.Instance[LocaleKeys.SettingsTabHotkeysCycleInputDevicePlayerX];
return string.Format(baseText, playerName);
}
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@ -110,5 +110,8 @@ namespace Ryujinx.Ava.UI.Helpers
[LibraryImport("user32.dll", SetLastError = true)]
public static partial nint SetWindowLongPtrW(nint hWnd, int nIndex, nint value);
[LibraryImport("user32.dll", SetLastError = true)]
public static partial ushort GetAsyncKeyState(int nVirtKey);
}
}

View File

@ -32,6 +32,24 @@ namespace Ryujinx.Ava.UI.Models.Input
[ObservableProperty] private bool _turboModeWhileHeld;
[ObservableProperty] private Key _cycleInputDevicePlayer1;
[ObservableProperty] private Key _cycleInputDevicePlayer2;
[ObservableProperty] private Key _cycleInputDevicePlayer3;
[ObservableProperty] private Key _cycleInputDevicePlayer4;
[ObservableProperty] private Key _cycleInputDevicePlayer5;
[ObservableProperty] private Key _cycleInputDevicePlayer6;
[ObservableProperty] private Key _cycleInputDevicePlayer7;
[ObservableProperty] private Key _cycleInputDevicePlayer8;
[ObservableProperty] private Key _cycleInputDeviceHandheld;
public HotkeyConfig(KeyboardHotkeys config)
{
if (config == null)
@ -50,6 +68,15 @@ namespace Ryujinx.Ava.UI.Models.Input
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
TurboMode = config.TurboMode;
TurboModeWhileHeld = config.TurboModeWhileHeld;
CycleInputDevicePlayer1 = config.CycleInputDevicePlayer1;
CycleInputDevicePlayer2 = config.CycleInputDevicePlayer2;
CycleInputDevicePlayer3 = config.CycleInputDevicePlayer3;
CycleInputDevicePlayer4 = config.CycleInputDevicePlayer4;
CycleInputDevicePlayer5 = config.CycleInputDevicePlayer5;
CycleInputDevicePlayer6 = config.CycleInputDevicePlayer6;
CycleInputDevicePlayer7 = config.CycleInputDevicePlayer7;
CycleInputDevicePlayer8 = config.CycleInputDevicePlayer8;
CycleInputDeviceHandheld = config.CycleInputDeviceHandheld;
}
public KeyboardHotkeys GetConfig() =>
@ -67,7 +94,16 @@ namespace Ryujinx.Ava.UI.Models.Input
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
TurboMode = TurboMode,
TurboModeWhileHeld = TurboModeWhileHeld
TurboModeWhileHeld = TurboModeWhileHeld,
CycleInputDevicePlayer1 = CycleInputDevicePlayer1,
CycleInputDevicePlayer2 = CycleInputDevicePlayer2,
CycleInputDevicePlayer3 = CycleInputDevicePlayer3,
CycleInputDevicePlayer4 = CycleInputDevicePlayer4,
CycleInputDevicePlayer5 = CycleInputDevicePlayer5,
CycleInputDevicePlayer6 = CycleInputDevicePlayer6,
CycleInputDevicePlayer7 = CycleInputDevicePlayer7,
CycleInputDevicePlayer8 = CycleInputDevicePlayer8,
CycleInputDeviceHandheld = CycleInputDeviceHandheld
};
}
}

View File

@ -0,0 +1,292 @@
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using OriginalInputConfig = Ryujinx.Common.Configuration.Hid.InputConfig;
using OriginalPlayerIndex = Ryujinx.Common.Configuration.Hid.PlayerIndex;
using OriginalInputBackendType = Ryujinx.Common.Configuration.Hid.InputBackendType;
using Ryujinx.Ava.Common.Locale;
namespace Ryujinx.UI.Overlay
{
/// <summary>
/// Controller overlay that shows controller bindings matching the original AXAML design
/// </summary>
public class ControllerOverlay : Overlay
{
private const float OverlayWidth = 400;
private const float Padding = 24;
private const float PlayerSpacing = 12;
private const float PlayerRowHeight = 32;
private const float TitleTextSize = 25;
private const float PlayerTextSize = 22;
private float _lifespan = 0f;
public ControllerOverlay() : base("ControllerOverlay")
{
CreateBaseElements();
}
private void CreateBaseElements()
{
// Main background container
var background = new RectangleElement(0, 0, OverlayWidth, 200, // Dynamic height will be set later
new SKColor(0, 0, 0, 224)) // #E0000000
{
Name = "Background",
CornerRadius = 12,
BorderColor = new SKColor(255, 255, 255, 64), // #40FFFFFF
BorderWidth = 1
};
AddElement(background);
// Title text (will be updated with localized text)
var titleText = new TextElement(Padding + 30, Padding, LocaleManager.Instance[LocaleKeys.ControllerOverlayTitle], TitleTextSize, SKColors.White)
{
Name = "TitleText",
FontStyle = SKFontStyle.Bold
};
AddElement(titleText);
}
/// <summary>
/// Show controller bindings with localized strings
/// </summary>
public void ShowControllerBindings(List<OriginalInputConfig> inputConfigs, int durationSeconds)
{
// Update title text
var titleElement = FindElement<TextElement>("TitleText");
if (titleElement != null)
{
titleElement.Text = LocaleManager.Instance[LocaleKeys.ControllerOverlayTitle];
}
// Reset lifespan and opacity
_lifespan = durationSeconds;
// Clear existing player bindings
ClearPlayerBindings();
// Group controllers by player index (support all players + handheld)
var playerBindings = new Dictionary<OriginalPlayerIndex, List<OriginalInputConfig>>();
foreach (var config in inputConfigs.Where(c => c.PlayerIndex <= OriginalPlayerIndex.Handheld))
{
if (!playerBindings.ContainsKey(config.PlayerIndex))
{
playerBindings[config.PlayerIndex] = new List<OriginalInputConfig>();
}
playerBindings[config.PlayerIndex].Add(config);
}
float currentY = Padding + 40; // After title
// Add player bindings to UI (support 8 players + handheld)
var playerIndices = new[]
{
OriginalPlayerIndex.Player1, OriginalPlayerIndex.Player2, OriginalPlayerIndex.Player3, OriginalPlayerIndex.Player4,
OriginalPlayerIndex.Player5, OriginalPlayerIndex.Player6, OriginalPlayerIndex.Player7, OriginalPlayerIndex.Player8,
OriginalPlayerIndex.Handheld
};
for (int i = 0; i < playerIndices.Length; i++)
{
var playerIndex = playerIndices[i];
float rowY = currentY + (i * (PlayerRowHeight + PlayerSpacing));
// Player number with colored background (circular badge)
var playerColor = GetPlayerColor(i);
var playerBadge = new RectangleElement(Padding, rowY, 24, 20, playerColor)
{
Name = $"PlayerBadge_{i}",
CornerRadius = 12
};
AddElement(playerBadge);
// Player number text
string playerLabel = playerIndex == OriginalPlayerIndex.Handheld ? "H" : $"P{(int)playerIndex + 1}";
var playerLabelElement = new TextElement(Padding + 12, rowY + 2, playerLabel, PlayerTextSize, SKColors.White)
{
Name = $"PlayerLabel_{i}",
FontStyle = SKFontStyle.Bold,
TextAlign = SKTextAlign.Center
};
AddElement(playerLabelElement);
// Controller info
if (playerBindings.ContainsKey(playerIndex))
{
var controllers = playerBindings[playerIndex];
var controllerNames = GetUniqueControllerDisplayNames(controllers);
var controllerTextElement = new TextElement(Padding + 56, rowY + 2, string.Join(", ", controllerNames), PlayerTextSize, new SKColor(144, 238, 144)) // LightGreen
{
Name = $"ControllerText_{i}",
FontStyle = SKFontStyle.Bold
};
AddElement(controllerTextElement);
}
else
{
var noControllerTextElement = new TextElement(Padding + 56, rowY + 2, LocaleManager.Instance[LocaleKeys.ControllerOverlayNoController], PlayerTextSize, new SKColor(128, 128, 128)) // Gray
{
Name = $"NoControllerText_{i}",
FontStyle = SKFontStyle.Italic
};
AddElement(noControllerTextElement);
}
}
// Calculate total height and update background
float totalHeight = Padding + 40 + (playerIndices.Length * (PlayerRowHeight + PlayerSpacing)) + Padding + 20;
var background = FindElement<RectangleElement>("Background");
if (background != null)
{
background.Height = totalHeight;
}
// Show the overlay (position will be set by Window class with actual dimensions)
IsVisible = true;
}
private static SKColor GetPlayerColor(int playerIndex)
{
return playerIndex switch
{
0 => new SKColor(255, 92, 92), // Red for Player 1
1 => new SKColor(54, 162, 235), // Blue for Player 2
2 => new SKColor(255, 206, 84), // Yellow for Player 3
3 => new SKColor(75, 192, 192), // Green for Player 4
4 => new SKColor(153, 102, 255), // Purple for Player 5
5 => new SKColor(255, 159, 64), // Orange for Player 6
6 => new SKColor(199, 199, 199), // Light Gray for Player 7
7 => new SKColor(83, 102, 255), // Indigo for Player 8
8 => new SKColor(255, 99, 132), // Pink for Handheld
_ => new SKColor(128, 128, 128) // Gray fallback
};
}
private List<string> GetUniqueControllerDisplayNames(List<OriginalInputConfig> controllers)
{
var nameGroups = new Dictionary<string, List<int>>();
var displayNames = new List<string>();
// First pass: get base names and group them
for (int i = 0; i < controllers.Count; i++)
{
string baseName = GetControllerDisplayName(controllers[i]);
if (!nameGroups.ContainsKey(baseName))
{
nameGroups[baseName] = new List<int>();
}
nameGroups[baseName].Add(i);
displayNames.Add(baseName);
}
// Second pass: add numbering for duplicates
foreach (var group in nameGroups.Where(g => g.Value.Count > 1))
{
for (int i = 0; i < group.Value.Count; i++)
{
int index = group.Value[i];
displayNames[index] = $"{group.Key} #{i + 1}";
}
}
return displayNames;
}
private string GetControllerDisplayName(OriginalInputConfig config)
{
if (string.IsNullOrEmpty(config.Name))
{
return config.Backend switch
{
OriginalInputBackendType.WindowKeyboard => LocaleManager.Instance[LocaleKeys.ControllerOverlayKeyboard],
OriginalInputBackendType.GamepadSDL2 => LocaleManager.Instance[LocaleKeys.ControllerOverlayController],
_ => LocaleManager.Instance[LocaleKeys.ControllerOverlayUnknown]
};
}
// Truncate long controller names from the middle
string name = config.Name;
if (name.Length > 25)
{
int keepLength = 22; // Total characters to keep (excluding "...")
int leftLength = (keepLength + 1) / 2; // Round up for left side
int rightLength = keepLength / 2; // Round down for right side
name = name.Substring(0, leftLength) + "..." + name.Substring(name.Length - rightLength);
}
return name;
}
/// <summary>
/// Clear all player bindings
/// </summary>
private void ClearPlayerBindings()
{
var elementsToRemove = new List<OverlayElement>();
foreach (var element in GetElements())
{
if (element.Name.StartsWith("PlayerBadge_") ||
element.Name.StartsWith("PlayerLabel_") ||
element.Name.StartsWith("ControllerText_") ||
element.Name.StartsWith("NoControllerText_"))
{
elementsToRemove.Add(element);
}
}
foreach (var element in elementsToRemove)
{
RemoveElement(element);
element.Dispose();
}
}
/// <summary>
/// Update overlay
/// </summary>
public override void Update(float deltaTime, SKSize screenSize)
{
_lifespan -= deltaTime;
if (_lifespan <= 0)
{
IsVisible = false;
return;
}
if (_lifespan <= 0.5f)
{
// Fade out during the last 0.5 seconds
Opacity = _lifespan / 0.5f;
}
else
{
Opacity = 1;
}
// Update position if screen size is provided
if (screenSize.Width > 0 && screenSize.Height > 0)
{
SetPositionToTopRight(screenSize.Width, screenSize.Height);
}
}
/// <summary>
/// Position overlay to top-right matching original AXAML positioning
/// </summary>
public void SetPositionToTopRight(float screenWidth, float screenHeight)
{
X = screenWidth - OverlayWidth - 20; // 20px margin from right
Y = 50; // 50px margin from top
}
}
}

View File

@ -0,0 +1,146 @@
using Ryujinx.Common.Logging;
using SkiaSharp;
using System;
namespace Ryujinx.UI.Overlay
{
/// <summary>
/// Image overlay element
/// </summary>
public class ImageElement : OverlayElement
{
private SKBitmap _bitmap;
private byte[] _imageData;
private string _imagePath;
public SKFilterQuality FilterQuality { get; set; } = SKFilterQuality.Medium;
public bool MaintainAspectRatio { get; set; } = true;
public ImageElement()
{
}
public ImageElement(float x, float y, float width, float height, byte[] imageData)
{
X = x;
Y = y;
Width = width;
Height = height;
SetImageData(imageData);
}
public ImageElement(float x, float y, float width, float height, string imagePath)
{
X = x;
Y = y;
Width = width;
Height = height;
SetImagePath(imagePath);
}
/// <summary>
/// Set image from byte array
/// </summary>
public void SetImageData(byte[] imageData)
{
_imageData = imageData;
_imagePath = null;
LoadBitmap();
}
/// <summary>
/// Set image from file path
/// </summary>
public void SetImagePath(string imagePath)
{
_imagePath = imagePath;
_imageData = null;
LoadBitmap();
}
/// <summary>
/// Set image from existing SKBitmap
/// </summary>
public void SetBitmap(SKBitmap bitmap)
{
_bitmap?.Dispose();
_bitmap = bitmap;
_imageData = null;
_imagePath = null;
}
private void LoadBitmap()
{
try
{
_bitmap?.Dispose();
_bitmap = null;
if (_imageData != null)
{
_bitmap = SKBitmap.Decode(_imageData);
}
else if (!string.IsNullOrEmpty(_imagePath))
{
_bitmap = SKBitmap.Decode(_imagePath);
}
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.Gpu, $"Failed to load image: {ex.Message}");
_bitmap = null;
}
}
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
{
if (!IsVisible || _bitmap == null || Width <= 0 || Height <= 0)
return;
float effectiveOpacity = Opacity * globalOpacity;
using var paint = new SKPaint
{
FilterQuality = FilterQuality,
Color = SKColors.White.WithAlpha((byte)(255 * effectiveOpacity))
};
var sourceRect = new SKRect(0, 0, _bitmap.Width, _bitmap.Height);
var destRect = new SKRect(X, Y, X + Width, Y + Height);
if (MaintainAspectRatio)
{
// Calculate aspect ratio preserving destination rectangle
float sourceAspect = (float)_bitmap.Width / _bitmap.Height;
float destAspect = Width / Height;
if (sourceAspect > destAspect)
{
// Source is wider, fit to width
float newHeight = Width / sourceAspect;
float yOffset = (Height - newHeight) / 2;
destRect = new SKRect(X, Y + yOffset, X + Width, Y + yOffset + newHeight);
}
else
{
// Source is taller, fit to height
float newWidth = Height * sourceAspect;
float xOffset = (Width - newWidth) / 2;
destRect = new SKRect(X + xOffset, Y, X + xOffset + newWidth, Y + Height);
}
}
canvas.DrawBitmap(_bitmap, sourceRect, destRect, paint);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_bitmap?.Dispose();
_bitmap = null;
}
base.Dispose(disposing);
}
}
}

View File

@ -0,0 +1,116 @@
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using Ryujinx.Graphics.Gpu;
namespace Ryujinx.UI.Overlay
{
/// <summary>
/// Base overlay class containing multiple elements
/// </summary>
public abstract class Overlay : IOverlay
{
private readonly List<OverlayElement> _elements = new();
public string Name { get; set; } = string.Empty;
public bool IsVisible { get; set; } = true;
public float Opacity { get; set; } = 1.0f;
public float X { get; set; }
public float Y { get; set; }
public int ZIndex { get; set; } = 0;
public Overlay()
{
}
public Overlay(string name)
{
Name = name;
}
/// <summary>
/// Add an element to this overlay
/// </summary>
public void AddElement(OverlayElement element)
{
_elements.Add(element);
}
/// <summary>
/// Remove an element from this overlay
/// </summary>
public void RemoveElement(OverlayElement element)
{
_elements.Remove(element);
}
/// <summary>
/// Get all elements
/// </summary>
public IReadOnlyList<OverlayElement> GetElements()
{
return _elements.AsReadOnly();
}
/// <summary>
/// Find element by name
/// </summary>
public T FindElement<T>(string name) where T : OverlayElement
{
return _elements.OfType<T>().FirstOrDefault(e => e.Name == name);
}
/// <summary>
/// Clear all elements
/// </summary>
public void Clear()
{
foreach (var element in _elements)
{
element.Dispose();
}
_elements.Clear();
}
/// <summary>
/// Update overlay
/// </summary>
public abstract void Update(float deltaTime, SKSize screenSize = default);
/// <summary>
/// Render this overlay
/// </summary>
public void Render(SKCanvas canvas)
{
if (!IsVisible || Opacity <= 0.0f)
return;
// Save canvas state
canvas.Save();
// Apply overlay position offset
if (X != 0 || Y != 0)
{
canvas.Translate(X, Y);
}
// Render all elements
foreach (var element in _elements)
{
if (element.IsVisible)
{
element.Render(canvas, Opacity);
}
}
// Restore canvas state
canvas.Restore();
}
public void Dispose()
{
Clear();
}
}
}

View File

@ -0,0 +1,66 @@
using SkiaSharp;
using System;
namespace Ryujinx.UI.Overlay
{
/// <summary>
/// Base class for all overlay elements
/// </summary>
public abstract class OverlayElement : IDisposable
{
public float X { get; set; }
public float Y { get; set; }
public float Width { get; set; }
public float Height { get; set; }
public bool IsVisible { get; set; } = true;
public float Opacity { get; set; } = 1.0f;
public string Name { get; set; } = string.Empty;
/// <summary>
/// Render this element to the canvas
/// </summary>
/// <param name="canvas">The canvas to draw on</param>
/// <param name="globalOpacity">Global opacity multiplier</param>
public abstract void Render(SKCanvas canvas, float globalOpacity = 1.0f);
/// <summary>
/// Check if a point is within this element's bounds
/// </summary>
public virtual bool Contains(float x, float y)
{
return x >= X && x <= X + Width && y >= Y && y <= Y + Height;
}
/// <summary>
/// Get the bounds of this element
/// </summary>
public SKRect GetBounds()
{
return new SKRect(X, Y, X + Width, Y + Height);
}
/// <summary>
/// Apply opacity to a color
/// </summary>
protected SKColor ApplyOpacity(SKColor color, float opacity)
{
return color.WithAlpha((byte)(color.Alpha * opacity));
}
/// <summary>
/// Dispose of resources
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose of resources
/// </summary>
protected virtual void Dispose(bool disposing)
{
}
}
}

View File

@ -0,0 +1,161 @@
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Graphics.Gpu;
namespace Ryujinx.UI.Overlay
{
/// <summary>
/// Manages multiple overlays and handles rendering
/// </summary>
public class OverlayManager : IOverlayManager
{
private readonly List<IOverlay> _overlays = new();
private readonly object _lock = new();
/// <summary>
/// Add an overlay to the manager
/// </summary>
public void AddOverlay(IOverlay overlay)
{
lock (_lock)
{
_overlays.Add(overlay);
SortOverlays();
}
}
/// <summary>
/// Remove an overlay from the manager
/// </summary>
public void RemoveOverlay(Overlay overlay)
{
lock (_lock)
{
_overlays.Remove(overlay);
}
}
/// <summary>
/// Remove overlay by name
/// </summary>
public void RemoveOverlay(string name)
{
lock (_lock)
{
var overlay = _overlays.FirstOrDefault(o => o.Name == name);
if (overlay != null)
{
_overlays.Remove(overlay);
overlay.Dispose();
}
}
}
/// <summary>
/// Find overlay by name
/// </summary>
public IOverlay FindOverlay(string name)
{
lock (_lock)
{
return _overlays.FirstOrDefault(o => o.Name == name);
}
}
/// <summary>
/// Get all overlays
/// </summary>
public IReadOnlyList<IOverlay> GetOverlays()
{
lock (_lock)
{
return _overlays.AsReadOnly();
}
}
/// <summary>
/// Clear all overlays
/// </summary>
public void Clear()
{
lock (_lock)
{
foreach (var overlay in _overlays)
{
overlay.Dispose();
}
_overlays.Clear();
}
}
/// <summary>
/// Update all overlays (for animations)
/// </summary>
public void Update(float deltaTime, SKSize screenSize = default)
{
lock (_lock)
{
foreach (var overlay in _overlays.Where(o => o.IsVisible))
{
overlay.Update(deltaTime, screenSize);
}
}
}
/// <summary>
/// Render all visible overlays
/// </summary>
public void Render(SKCanvas canvas)
{
lock (_lock)
{
foreach (var overlay in _overlays.Where(o => o.IsVisible && o.Opacity > 0.0f))
{
overlay.Render(canvas);
}
}
}
/// <summary>
/// Sort overlays by Z-index
/// </summary>
private void SortOverlays()
{
_overlays.Sort((a, b) => a.ZIndex.CompareTo(b.ZIndex));
}
/// <summary>
/// Show overlay
/// </summary>
public void ShowOverlay(string name)
{
var overlay = FindOverlay(name);
if (overlay != null)
{
overlay.IsVisible = true;
overlay.Opacity = 1.0f;
}
}
/// <summary>
/// Hide overlay
/// </summary>
public void HideOverlay(string name)
{
var overlay = FindOverlay(name);
if (overlay != null)
{
overlay.IsVisible = false;
overlay.Opacity = 0.0f;
}
}
public void Dispose()
{
Clear();
}
}
}

View File

@ -0,0 +1,78 @@
using SkiaSharp;
namespace Ryujinx.UI.Overlay
{
/// <summary>
/// Rectangle overlay element
/// </summary>
public class RectangleElement : OverlayElement
{
public SKColor BackgroundColor { get; set; } = SKColors.Transparent;
public SKColor BorderColor { get; set; } = SKColors.Transparent;
public float BorderWidth { get; set; } = 0;
public float CornerRadius { get; set; } = 0;
public RectangleElement()
{
}
public RectangleElement(float x, float y, float width, float height, SKColor backgroundColor)
{
X = x;
Y = y;
Width = width;
Height = height;
BackgroundColor = backgroundColor;
}
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
{
if (!IsVisible || Width <= 0 || Height <= 0)
return;
float effectiveOpacity = Opacity * globalOpacity;
var bounds = new SKRect(X, Y, X + Width, Y + Height);
// Draw background
if (BackgroundColor.Alpha > 0)
{
using var backgroundPaint = new SKPaint
{
Color = ApplyOpacity(BackgroundColor, effectiveOpacity),
Style = SKPaintStyle.Fill,
IsAntialias = true
};
if (CornerRadius > 0)
{
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, backgroundPaint);
}
else
{
canvas.DrawRect(bounds, backgroundPaint);
}
}
// Draw border
if (BorderWidth > 0 && BorderColor.Alpha > 0)
{
using var borderPaint = new SKPaint
{
Color = ApplyOpacity(BorderColor, effectiveOpacity),
Style = SKPaintStyle.Stroke,
StrokeWidth = BorderWidth,
IsAntialias = true
};
if (CornerRadius > 0)
{
canvas.DrawRoundRect(bounds, CornerRadius, CornerRadius, borderPaint);
}
else
{
canvas.DrawRect(bounds, borderPaint);
}
}
}
}
}

View File

@ -0,0 +1,121 @@
using SkiaSharp;
namespace Ryujinx.UI.Overlay
{
/// <summary>
/// Text overlay element
/// </summary>
public class TextElement : OverlayElement
{
public string Text { get; set; } = string.Empty;
public SKColor TextColor { get; set; } = SKColors.White;
public float FontSize { get; set; } = 16;
public string FontFamily { get; set; } = "Arial";
public SKFontStyle FontStyle { get; set; } = SKFontStyle.Normal;
public SKTextAlign TextAlign { get; set; } = SKTextAlign.Left;
public bool IsAntialias { get; set; } = true;
// Shadow properties
public bool HasShadow { get; set; } = false;
public SKColor ShadowColor { get; set; } = SKColors.Black;
public float ShadowOffsetX { get; set; } = 1;
public float ShadowOffsetY { get; set; } = 1;
public float ShadowBlur { get; set; } = 0;
public TextElement()
{
}
public TextElement(float x, float y, string text, float fontSize = 16, SKColor? color = null)
{
X = x;
Y = y;
Text = text;
FontSize = fontSize;
TextColor = color ?? SKColors.White;
// Auto-calculate width and height based on text
UpdateDimensions();
}
public override void Render(SKCanvas canvas, float globalOpacity = 1.0f)
{
if (!IsVisible || string.IsNullOrEmpty(Text))
return;
float effectiveOpacity = Opacity * globalOpacity;
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
using var paint = new SKPaint
{
Color = ApplyOpacity(TextColor, effectiveOpacity),
TextSize = FontSize,
Typeface = typeface,
TextAlign = TextAlign,
IsAntialias = IsAntialias
};
float textX = X;
float textY = Y + FontSize; // Baseline adjustment
// Adjust X position based on alignment
if (TextAlign == SKTextAlign.Center)
{
textX += Width / 2;
}
else if (TextAlign == SKTextAlign.Right)
{
textX += Width;
}
// Draw shadow if enabled
if (HasShadow)
{
using var shadowPaint = new SKPaint
{
Color = ApplyOpacity(ShadowColor, effectiveOpacity),
TextSize = FontSize,
Typeface = typeface,
TextAlign = TextAlign,
IsAntialias = IsAntialias
};
if (ShadowBlur > 0)
{
shadowPaint.MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, ShadowBlur);
}
canvas.DrawText(Text, textX + ShadowOffsetX, textY + ShadowOffsetY, shadowPaint);
}
// Draw main text
canvas.DrawText(Text, textX, textY, paint);
}
/// <summary>
/// Update width and height based on current text and font settings
/// </summary>
public void UpdateDimensions()
{
if (string.IsNullOrEmpty(Text))
{
Width = 0;
Height = 0;
return;
}
using var typeface = SKTypeface.FromFamilyName(FontFamily, FontStyle);
using var paint = new SKPaint
{
TextSize = FontSize,
Typeface = typeface
};
var bounds = new SKRect();
paint.MeasureText(Text, ref bounds);
Width = bounds.Width;
Height = FontSize; // Use font size as height for consistency
}
}
}

View File

@ -4,6 +4,7 @@ using Ryujinx.Ava.Systems.AppLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
using Ryujinx.Ava.Common.Locale;
namespace Ryujinx.Ava.UI.ViewModels
{
@ -11,15 +12,37 @@ namespace Ryujinx.Ava.UI.ViewModels
{
private readonly ApplicationLibrary _appLibrary;
private (int Status, int Name) _sorting;
public bool IsSortedByTitle => true;
public bool IsSortedByStatus => true;
// Avalonia takes names of status from these variables
public LocaleKeys IsStringPlayable => LocaleKeys.CompatibilityListPlayable;
public LocaleKeys IsStringInGame => LocaleKeys.CompatibilityListIngame;
public LocaleKeys IsStringMenus => LocaleKeys.CompatibilityListMenus;
public LocaleKeys IsStringBoots => LocaleKeys.CompatibilityListBoots;
public LocaleKeys IsStringNothing => LocaleKeys.CompatibilityListNothing;
public string PlayableInfoText { get; set; }
public string InGameInfoText { get; set; }
public string MenusInfoText { get; set; }
public string BootsInfoText { get; set; }
public string NothingInfoText { get; set; }
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityDatabase.Entries;
private string[] _ownedGameTitleIds = [];
private Func<CompatibilityEntry, object> _sortKeySelector = x => x.GameName; // Default sort by GameName
public IEnumerable<CompatibilityEntry> CurrentEntries => OnlyShowOwnedGames
? _currentEntries.Where(x =>
x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid)))
: _currentEntries;
public CompatibilityViewModel() { }
public CompatibilityViewModel() {}
private void AppCountUpdated(object _, ApplicationCountUpdatedEventArgs __)
=> _ownedGameTitleIds = _appLibrary.Applications.Keys.Select(x => x.ToString("X16")).ToArray();
@ -27,19 +50,29 @@ namespace Ryujinx.Ava.UI.ViewModels
public CompatibilityViewModel(ApplicationLibrary appLibrary)
{
_appLibrary = appLibrary;
AppCountUpdated(null, null);
CountByStatus();
_appLibrary.ApplicationCountUpdated += AppCountUpdated;
}
public void CountByStatus()
{
PlayableInfoText = LocaleManager.Instance[LocaleKeys.CompatibilityListPlayable] + ": " + CurrentEntries.Count(x => x.Status == LocaleKeys.CompatibilityListPlayable);
InGameInfoText = LocaleManager.Instance[LocaleKeys.CompatibilityListIngame] + ": " + CurrentEntries.Count(x => x.Status == LocaleKeys.CompatibilityListIngame);
MenusInfoText = LocaleManager.Instance[LocaleKeys.CompatibilityListMenus] + ": " + CurrentEntries.Count(x => x.Status == LocaleKeys.CompatibilityListMenus);
BootsInfoText = LocaleManager.Instance[LocaleKeys.CompatibilityListBoots] + ": " + CurrentEntries.Count(x => x.Status == LocaleKeys.CompatibilityListBoots);
NothingInfoText = LocaleManager.Instance[LocaleKeys.CompatibilityListNothing] + ": " + CurrentEntries.Count(x => x.Status == LocaleKeys.CompatibilityListNothing);
_onlyShowOwnedGames = true;
}
void IDisposable.Dispose()
{
GC.SuppressFinalize(this);
_appLibrary.ApplicationCountUpdated -= AppCountUpdated;
}
private bool _onlyShowOwnedGames = true;
private bool _onlyShowOwnedGames;
public bool OnlyShowOwnedGames
{
@ -54,17 +87,37 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public void NameSorting(int nameSort = 0)
{
_sorting.Name = nameSort;
SortApply();
OnPropertyChanged();
OnPropertyChanged(nameof(SortName));
}
public void StatusSorting(int statusSort = 0)
{
_sorting.Status = statusSort;
SortApply();
OnPropertyChanged();
OnPropertyChanged(nameof(SortName));
}
public void Search(string searchTerm)
{
if (string.IsNullOrEmpty(searchTerm))
{
SetEntries(CompatibilityDatabase.Entries);
SortApply();
return;
}
SetEntries(CompatibilityDatabase.Entries.Where(x =>
x.GameName.ContainsIgnoreCase(searchTerm)
|| x.TitleId.Check(tid => tid.ContainsIgnoreCase(searchTerm))));
SortApply();
}
private void SetEntries(IEnumerable<CompatibilityEntry> entries)
@ -72,5 +125,43 @@ namespace Ryujinx.Ava.UI.ViewModels
_currentEntries = entries.ToList();
OnPropertyChanged(nameof(CurrentEntries));
}
private void SortApply()
{
try
{
_currentEntries = (_sorting switch
{
(0, 0) => _currentEntries.OrderBy(x => _sortKeySelector(x) ?? string.Empty), // A - Z
(0, 1) => _currentEntries.OrderByDescending(x => _sortKeySelector(x) ?? string.Empty), // Z - A
(1, 0) => _currentEntries.OrderBy(x => x.Status).ThenBy(x => x.GameName, StringComparer.OrdinalIgnoreCase), // Status Playable - Nothing, then A - Z
(1, 1) => _currentEntries.OrderBy(x => x.Status).ThenByDescending(x => x.GameName, StringComparer.OrdinalIgnoreCase), // Status Nothing - Playable, then A - Z
(2, 0) => _currentEntries.OrderByDescending(x => x.Status).ThenBy(x => x.GameName, StringComparer.OrdinalIgnoreCase), // Status Playable - Nothing, then Z - A
(2, 1) => _currentEntries.OrderByDescending(x => x.Status).ThenByDescending(x => x.GameName, StringComparer.OrdinalIgnoreCase), // Status Nothing - Playable, then Z - A
_ => _currentEntries.OrderBy(x => x.Status)
}).ToList();
}
catch (Exception)
{
}
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentEntries));
}
public string SortName
{
get
{
return (_sorting.Name) switch
{
(0) => LocaleManager.Instance[LocaleKeys.GameListSortStatusNameAscending],
(1) => LocaleManager.Instance[LocaleKeys.GameListSortStatusNameDescending],
_ => string.Empty,
};
}
}
}
}

View File

@ -50,6 +50,9 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private string _controllerImage;
private int _device;
private object _configViewModel;
private bool _isChangeTrackingActive;
private string _chosenProfile;
[ObservableProperty] private bool _isModified;
[ObservableProperty] private string _profileName;
[ObservableProperty] private bool _notificationIsVisible; // Automatically call the NotificationView property with OnPropertyChanged()
[ObservableProperty] private string _notificationText; // Automatically call the NotificationText property with OnPropertyChanged()
@ -84,6 +87,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public AvaloniaList<string> ProfilesList { get; set; }
public AvaloniaList<string> DeviceList { get; set; }
public bool UseGlobalConfig;
// XAML Flags
public bool ShowSettings => _device > 0;
public bool IsController => _device > 1;
@ -94,31 +99,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool HasLed => SelectedGamepad.Features.HasFlag(GamepadFeaturesFlag.Led);
public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense");
public bool _isChangeTrackingActive;
public bool _isModified;
public bool IsModified
{
get => _isModified;
set
{
_isModified = value;
OnPropertyChanged();
}
}
public event Action NotifyChangesEvent;
public string _profileChoose;
public string ProfileChoose
public string ChosenProfile
{
get => _profileChoose;
get => _chosenProfile;
set
{
// When you select a profile, the settings from the profile will be applied.
// To save the settings, you still need to click the apply button
_profileChoose = value;
_chosenProfile = value;
LoadProfile();
OnPropertyChanged();
}
@ -290,7 +280,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public InputConfig Config { get; set; }
public InputViewModel(UserControl owner) : this()
public InputViewModel(UserControl owner, bool useGlobal = false) : this()
{
if (Program.PreviewerDetached)
{
@ -303,6 +293,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_mainWindow.ViewModel.AppHost?.NpadManager.BlockInputUpdates();
UseGlobalConfig = useGlobal;
_isLoaded = false;
LoadDevices();
@ -335,9 +327,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
PlayerIndexes.Add(new(PlayerIndex.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsHandheld]));
}
private void LoadConfiguration(InputConfig inputConfig = null)
{
if (UseGlobalConfig && Program.UseExtraConfig)
{
Config = inputConfig ?? ConfigurationState.InstanceExtra.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
}
else
{
Config = inputConfig ?? ConfigurationState.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
}
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
{
@ -525,18 +526,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
}
private static string GetShortGamepadName(string str)
{
const string Ellipsis = "...";
const int MaxSize = 50;
if (str.Length > MaxSize)
{
return $"{str.AsSpan(0, MaxSize - Ellipsis.Length)}{Ellipsis}";
}
return str;
}
private static string GetShortGamepadId(string str)
{
@ -550,7 +540,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
string GetGamepadName(IGamepad gamepad, int controllerNumber)
{
return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
return $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
}
string GetUniqueGamepadName(IGamepad gamepad, ref int controllerNumber)
@ -578,7 +568,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (gamepad != null)
{
Devices.Add((DeviceType.Keyboard, id, $"{GetShortGamepadName(gamepad.Name)}"));
Devices.Add((DeviceType.Keyboard, id, $"{DefaultInputConfigurationProvider.GetShortGamepadName(gamepad.Name)}"));
}
}
@ -651,138 +641,21 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
InputConfig config;
if (activeDevice.Type == DeviceType.Keyboard)
{
string id = activeDevice.Id;
string name = activeDevice.Name;
config = new StandardKeyboardInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.WindowKeyboard,
Id = id,
Name = name,
ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
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<Key>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
{
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<Key>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
},
};
config = DefaultInputConfigurationProvider.CreateDefaultKeyboardConfig(activeDevice.Id, activeDevice.Name, _playerId);
}
else if (activeDevice.Type == DeviceType.Controller)
{
bool isNintendoStyle = Devices.ToList().FirstOrDefault(x => x.Id == activeDevice.Id).Name.Contains("Nintendo");
string id = activeDevice.Id.Split(" ")[0];
string name = activeDevice.Name;
config = new StandardControllerInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL2,
Id = id,
Name = name,
ControllerType = ControllerType.ProController,
DeadzoneLeft = 0.1f,
DeadzoneRight = 0.1f,
RangeLeft = 1.0f,
RangeRight = 1.0f,
TriggerThreshold = 0.5f,
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
{
DpadUp = ConfigGamepadInputId.DpadUp,
DpadDown = ConfigGamepadInputId.DpadDown,
DpadLeft = ConfigGamepadInputId.DpadLeft,
DpadRight = ConfigGamepadInputId.DpadRight,
ButtonMinus = ConfigGamepadInputId.Minus,
ButtonL = ConfigGamepadInputId.LeftShoulder,
ButtonZl = ConfigGamepadInputId.LeftTrigger,
ButtonSl = ConfigGamepadInputId.Unbound,
ButtonSr = ConfigGamepadInputId.Unbound,
},
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Left,
StickButton = ConfigGamepadInputId.LeftStick,
InvertStickX = false,
InvertStickY = false,
},
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
{
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
ButtonPlus = ConfigGamepadInputId.Plus,
ButtonR = ConfigGamepadInputId.RightShoulder,
ButtonZr = ConfigGamepadInputId.RightTrigger,
ButtonSl = ConfigGamepadInputId.Unbound,
ButtonSr = ConfigGamepadInputId.Unbound,
},
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Right,
StickButton = ConfigGamepadInputId.RightStick,
InvertStickX = false,
InvertStickY = false,
},
Motion = new StandardMotionConfigController
{
MotionBackend = MotionInputBackendType.GamepadDriver,
EnableMotion = true,
Sensitivity = 100,
GyroDeadzone = 1,
},
Rumble = new RumbleConfigController
{
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false,
},
};
bool isNintendoStyle = DefaultInputConfigurationProvider.IsNintendoStyleController(activeDevice.Name);
config = DefaultInputConfigurationProvider.CreateDefaultControllerConfig(activeDevice.Id, activeDevice.Name, _playerId, isNintendoStyle);
}
else
{
config = new InputConfig();
config = new InputConfig
{
PlayerIndex = _playerId
};
}
config.PlayerIndex = _playerId;
return config;
}
@ -902,7 +775,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
LoadProfiles();
ProfileChoose = ProfileName; // Show new profile
ChosenProfile = ProfileName; // Show new profile
}
else
{
@ -936,7 +809,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
LoadProfiles();
ProfileChoose = ProfilesList[0].ToString(); // Show default profile
ChosenProfile = ProfilesList[0].ToString(); // Show default profile
}
}
@ -966,7 +839,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
List<InputConfig> newConfig = [];
if (UseGlobalConfig && Program.UseExtraConfig)
{
newConfig.AddRange(ConfigurationState.InstanceExtra.Hid.InputConfig.Value);
}
else
{
newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value);
}
newConfig.Remove(newConfig.FirstOrDefault(x => x == null));
@ -1007,18 +887,21 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
}
_mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
// Atomically replace and signal input change.
// NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event.
ConfigurationState.Instance.Hid.InputConfig.Value = newConfig;
_mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
if (UseGlobalConfig && Program.UseExtraConfig)
{
// In User Settings when "Use Global Input" is enabled, it saves global input to global setting
ConfigurationState.InstanceExtra.Hid.InputConfig.Value = newConfig;
ConfigurationState.InstanceExtra.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
}
else
{
ConfigurationState.Instance.Hid.InputConfig.Value = newConfig;
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
}
public void NotifyChange(string property)
{
OnPropertyChanged(property);
}
public void NotifyChanges()

View File

@ -32,6 +32,7 @@ using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Common.UI;
@ -310,10 +311,15 @@ namespace Ryujinx.Ava.UI.ViewModels
private void TotalTimePlayed_Recalculated(Optional<TimeSpan> ts)
{
ShowTotalTimePlayed = ts.HasValue;
if (ts.HasValue)
LocaleManager.Instance.SetDynamicValues(LocaleKeys.GameListLabelTotalTimePlayed, ValueFormatUtils.FormatTimeSpan(ts.Value));
{
var formattedPlayTime = ValueFormatUtils.FormatTimeSpan(ts.Value);
LocaleManager.Instance.SetDynamicValues(LocaleKeys.GameListLabelTotalTimePlayed, formattedPlayTime);
ShowTotalTimePlayed = formattedPlayTime != string.Empty;
return;
}
ShowTotalTimePlayed = ts.HasValue;
}
public bool ShowTotalTimePlayed
@ -334,7 +340,6 @@ namespace Ryujinx.Ava.UI.ViewModels
_listSelectedApplication = value;
if (_listSelectedApplication != null && ListAppContextMenu == null)
ListAppContextMenu = new ApplicationContextMenu();
else if (_listSelectedApplication == null && ListAppContextMenu != null)
ListAppContextMenu = null!;
@ -1575,28 +1580,31 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool InitializeUserConfig(ApplicationData application)
{
// Code where conditions will be met before loading the user configuration (Global Config)
string BackendThreadingInit = Program.BackendThreadingArg;
BackendThreadingInit ??= ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString();
string backendThreadingInit = Program.BackendThreadingArg ?? ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString();
// If a configuration is found in the "/games/xxxxxxxxxxxxxx" folder, the program will load the user setting.
string idGame = application.IdBaseString;
if (ConfigurationFileFormat.TryLoad(Program.GetDirGameUserConfig(idGame), out ConfigurationFileFormat configurationFileFormat))
{
// Loads the user configuration, having previously changed the global configuration to the user configuration
ConfigurationState.Instance.Load(configurationFileFormat, Program.GetDirGameUserConfig(idGame, true, true), idGame);
ConfigurationState.Instance.Load(configurationFileFormat, Program.GetDirGameUserConfig(idGame, true), idGame);
if (ConfigurationFileFormat.TryLoad(Program.GlobalConfigurationPath, out ConfigurationFileFormat configurationFileFormatExtra))
{
//This is where the global configuration will be stored.
//This allows you to change the global configuration settings during the game (for example, the global input setting)
ConfigurationState.InstanceExtra.Load(configurationFileFormatExtra, Program.GlobalConfigurationPath);
}
}
// Code where conditions will be executed after loading user configuration
if (ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() != BackendThreadingInit)
if (ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() != backendThreadingInit)
{
List<string> Arguments = new()
{
"--bt", ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString() // BackendThreading
};
Rebooter.RebootAppWithGame(application.Path, Arguments);
Rebooter.RebootAppWithGame(application.Path,
[
"--bt",
ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString()
]);
return true;
}
@ -1683,10 +1691,33 @@ namespace Ryujinx.Ava.UI.ViewModels
SetMainContent(RendererHostControl);
RendererHostControl.Focus();
// Show controller overlay
ShowControllerOverlay();
});
public static void UpdateGameMetadata(string titleId)
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame());
public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime));
private void ShowControllerOverlay()
{
try
{
var inputConfigs = ConfigurationState.Instance.System.UseInputGlobalConfig.Value && Program.UseExtraConfig
? ConfigurationState.InstanceExtra.Hid.InputConfig.Value
: ConfigurationState.Instance.Hid.InputConfig.Value;
// Always show overlay - if no configs, it will show test data
int duration = ConfigurationState.Instance.ControllerOverlayGameStartDuration.Value;
// Show overlay through the GPU context window directly
AppHost.ShowControllerOverlay(inputConfigs, duration);
}
catch (Exception ex)
{
// Log the error but don't let it crash the game launch
Logger.Error?.Print(LogClass.UI, $"Failed to show controller overlay: {ex.Message}");
}
}
public void RefreshFirmwareStatus()
{
@ -1991,7 +2022,7 @@ namespace Ryujinx.Ava.UI.ViewModels
// just checking for file presence
viewModel.SelectedApplication.HasIndependentConfiguration = File.Exists(
Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString, false, false));
Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString));
viewModel.RefreshView();
});

View File

@ -53,6 +53,7 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] private bool _isVulkanAvailable = true;
[ObservableProperty] private bool _gameListNeedsRefresh;
private readonly List<string> _gpuIds = [];
public bool _useInputGlobalConfig;
private int _graphicsBackendIndex;
private int _scalingFilter;
private int _scalingFilterLevel;
@ -64,6 +65,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public event Action CloseWindow;
public event Action SaveSettingsEvent;
public event Action<bool> LocalGlobalInputSwitchEvent;
private int _networkInterfaceIndex;
private int _multiplayerModeIndex;
private string _ldnPassphrase;
@ -84,6 +86,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsGameTitleNotNull => !string.IsNullOrEmpty(GameTitle);
public double PanelOpacity => IsGameTitleNotNull ? 0.5 : 1;
public int ResolutionScale
{
get => _resolutionScale;
@ -142,8 +145,23 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableKeyboard { get; set; }
public bool EnableMouse { get; set; }
public bool DisableInputWhenOutOfFocus { get; set; }
public int FocusLostActionType { get; set; }
public int ControllerOverlayGameStartDuration { get; set; }
public int ControllerOverlayInputCycleDuration { get; set; }
public bool UseGlobalInputConfig
{
get => _useInputGlobalConfig;
set
{
_useInputGlobalConfig = value;
LocalGlobalInputSwitchEvent?.Invoke(_useInputGlobalConfig);
OnPropertyChanged(nameof(InputPanelOpacity));
OnPropertyChanged();
}
}
public double InputPanelOpacity => UseGlobalInputConfig ? 0.5 : 1;
public VSyncMode VSyncMode
{
@ -371,7 +389,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsInvalidLdnPassphraseVisible { get; set; }
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(false)
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
{
_virtualFileSystem = virtualFileSystem;
_contentManager = contentManager;
@ -392,7 +410,7 @@ namespace Ryujinx.Ava.UI.ViewModels
string gameName,
string gameId,
byte[] gameIconData,
bool enableToLoadCustomConfig) : this(enableToLoadCustomConfig)
bool customConfig) : this()
{
_virtualFileSystem = virtualFileSystem;
_contentManager = contentManager;
@ -408,9 +426,18 @@ namespace Ryujinx.Ava.UI.ViewModels
_gameTitle = gameName;
_gameId = gameId;
if (enableToLoadCustomConfig) // During the game. If there is no user config, then load the global config window
if (customConfig) // During the game. If there is no user config, then load the global config window
{
string gameDir = Program.GetDirGameUserConfig(gameId, false, true);
string gameDir = Program.GetDirGameUserConfig(gameId, true);
Program.UseExtraConfig = true;
if (ConfigurationFileFormat.TryLoad(Program.GlobalConfigurationPath, out ConfigurationFileFormat configurationFileFormatExtra))
{
// Extra load global configuration for input setting and save global input setting with other global config
ConfigurationState.InstanceExtra.Load(configurationFileFormatExtra, Program.GlobalConfigurationPath);
}
if (ConfigurationFileFormat.TryLoad(gameDir, out ConfigurationFileFormat configurationFileFormat))
{
ConfigurationState.Instance.Load(configurationFileFormat, gameDir, gameId);
@ -426,7 +453,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public SettingsViewModel(bool noLoadGlobalConfig = false)
public SettingsViewModel()
{
GameDirectories = [];
AutoloadDirectories = [];
@ -550,9 +577,9 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public void LoadCurrentConfiguration()
public void LoadCurrentConfiguration(bool global = false)
{
ConfigurationState config = ConfigurationState.Instance;
ConfigurationState config = global ? ConfigurationState.InstanceExtra: ConfigurationState.Instance;
// User Interface
EnableDiscordIntegration = config.EnableDiscordIntegration;
@ -562,6 +589,8 @@ namespace Ryujinx.Ava.UI.ViewModels
HideCursor = (int)config.HideCursor.Value;
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
FocusLostActionType = (int)config.FocusLostActionType.Value;
ControllerOverlayGameStartDuration = config.ControllerOverlayGameStartDuration.Value;
ControllerOverlayInputCycleDuration = config.ControllerOverlayInputCycleDuration.Value;
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
@ -578,6 +607,7 @@ namespace Ryujinx.Ava.UI.ViewModels
};
// Input
UseGlobalInputConfig = config.System.UseInputGlobalConfig;
EnableDockedMode = config.System.EnableDockedMode;
EnableKeyboard = config.Hid.EnableKeyboard;
EnableMouse = config.Hid.EnableMouse;
@ -660,9 +690,9 @@ namespace Ryujinx.Ava.UI.ViewModels
LdnServer = config.Multiplayer.LdnServer;
}
public void SaveSettings()
public void SaveSettings(bool global = false)
{
ConfigurationState config = ConfigurationState.Instance;
ConfigurationState config = global ? ConfigurationState.InstanceExtra: ConfigurationState.Instance;
// User Interface
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
@ -672,6 +702,8 @@ namespace Ryujinx.Ava.UI.ViewModels
config.HideCursor.Value = (HideCursorMode)HideCursor;
config.UpdateCheckerType.Value = (UpdaterType)UpdateCheckerType;
config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType;
config.ControllerOverlayGameStartDuration.Value = ControllerOverlayGameStartDuration;
config.ControllerOverlayInputCycleDuration.Value = ControllerOverlayInputCycleDuration;
config.UI.GameDirs.Value = [.. GameDirectories];
config.UI.AutoloadDirs.Value = [.. AutoloadDirectories];
@ -684,6 +716,7 @@ namespace Ryujinx.Ava.UI.ViewModels
};
// Input
config.System.UseInputGlobalConfig.Value = UseGlobalInputConfig;
config.System.EnableDockedMode.Value = EnableDockedMode;
config.Hid.EnableKeyboard.Value = EnableKeyboard;
config.Hid.EnableMouse.Value = EnableMouse;
@ -796,11 +829,14 @@ namespace Ryujinx.Ava.UI.ViewModels
private static void RevertIfNotSaved()
{
// maybe this is an unnecessary check(all options need to be tested)
/*
maybe this is an unnecessary check(all options need to be tested)
if (string.IsNullOrEmpty(Program.GlobalConfigurationPath))
{
Program.ReloadConfig();
}
*/
Program.ReloadConfig();
}
public void ApplyButton()
@ -810,7 +846,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public void DeleteConfigGame()
{
string gameDir = Program.GetDirGameUserConfig(GameId, false, false);
string gameDir = Program.GetDirGameUserConfig(GameId);
if (File.Exists(gameDir))
{

View File

@ -100,7 +100,7 @@
Name="ProfileBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
SelectedItem="{Binding ProfileChoose, Mode=TwoWay}"
SelectedItem="{Binding ChosenProfile, Mode=TwoWay}"
SelectionChanged="ComboBox_SelectionChanged"
ItemsSource="{Binding ProfilesList}"
Text="{Binding ProfileName, Mode=TwoWay}" />
@ -203,7 +203,6 @@
</StackPanel>
<ContentControl IsVisible="{Binding NotificationIsVisible}">
<ContentControl.Content>
<StackPanel>
<TextBlock
Margin="5,20,0,0"

View File

@ -1,6 +1,7 @@
using Avalonia.Controls;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
@ -14,7 +15,7 @@ namespace Ryujinx.Ava.UI.Views.Input
public InputView()
{
ViewModel = new InputViewModel(this);
ViewModel = new InputViewModel(this, ConfigurationState.Instance.System.UseInputGlobalConfig);
InitializeComponent();
}
@ -24,6 +25,13 @@ namespace Ryujinx.Ava.UI.Views.Input
ViewModel.Save();
}
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
{
Dispose();
ViewModel = new InputViewModel(this, enableConfigGlobal); // Create new Input Page with global input configs
InitializeComponent();
}
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (PlayerIndexBox != null)

View File

@ -64,6 +64,7 @@
MinWidth="200"
Height="6"
VerticalAlignment="Center"
Margin="0, 0, 5, 0"
Foreground="{DynamicResource SystemAccentColorLight2}"
IsVisible="{Binding StatusBarVisible}"
Maximum="{Binding StatusBarProgressMaximum}"

View File

@ -56,6 +56,26 @@
SelectedIndex="{Binding PreferredGpuIndex}"
ItemsSource="{Binding AvailableGpus}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale GraphicsBackendThreadingTooltip}"
Text="{ext:Locale SettingsTabGraphicsBackendMultithreading}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{ext:Locale GalThreadingTooltip}"
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
<ComboBoxItem>
<TextBlock Text="{ext:Locale CommonAuto}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale CommonOff}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale CommonOn}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabGraphicsFeatures}" />
@ -255,32 +275,7 @@
</ComboBox>
</StackPanel>
</StackPanel>
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale GraphicsBackendThreadingTooltip}"
Text="{ext:Locale SettingsTabGraphicsBackendMultithreading}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{ext:Locale GalThreadingTooltip}"
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
<ComboBoxItem>
<TextBlock Text="{ext:Locale CommonAuto}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale CommonOff}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale CommonOn}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabGraphicsDeveloperOptions}" />
<StackPanel

View File

@ -120,6 +120,60 @@
<TextBlock Text="{ext:Locale SettingsTabHotkeysOnlyWhilePressed}" Margin="10,0" />
<CheckBox IsChecked="{Binding KeyboardHotkey.TurboModeWhileHeld}" />
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale ControllerSettingsPlayer1, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
<ToggleButton Name="CycleInputDevicePlayer1">
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer1, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale ControllerSettingsPlayer2, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
<ToggleButton Name="CycleInputDevicePlayer2">
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer2, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale ControllerSettingsPlayer3, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
<ToggleButton Name="CycleInputDevicePlayer3">
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer3, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale ControllerSettingsPlayer4, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
<ToggleButton Name="CycleInputDevicePlayer4">
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer4, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale ControllerSettingsPlayer5, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
<ToggleButton Name="CycleInputDevicePlayer5">
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer5, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale ControllerSettingsPlayer6, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
<ToggleButton Name="CycleInputDevicePlayer6">
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer6, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale ControllerSettingsPlayer7, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
<ToggleButton Name="CycleInputDevicePlayer7">
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer7, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale ControllerSettingsPlayer8, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
<ToggleButton Name="CycleInputDevicePlayer8">
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDevicePlayer8, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale ControllerSettingsHandheld, Converter={x:Static helpers:PlayerHotkeyLabelConverter.Instance}}" Classes="settingHeader" />
<ToggleButton Name="CycleInputDeviceHandheld">
<TextBlock Text="{Binding KeyboardHotkey.CycleInputDeviceHandheld, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>

View File

@ -82,7 +82,16 @@ namespace Ryujinx.Ava.UI.Views.Settings
{ "VolumeDown", () => viewModel.KeyboardHotkey.VolumeDown = Key.Unbound },
{ "CustomVSyncIntervalIncrement", () => viewModel.KeyboardHotkey.CustomVSyncIntervalIncrement = Key.Unbound },
{ "CustomVSyncIntervalDecrement", () => viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = Key.Unbound },
{ "TurboMode", () => viewModel.KeyboardHotkey.TurboMode = Key.Unbound }
{ "TurboMode", () => viewModel.KeyboardHotkey.TurboMode = Key.Unbound },
{ "CycleInputDevicePlayer1", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer1 = Key.Unbound },
{ "CycleInputDevicePlayer2", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer2 = Key.Unbound },
{ "CycleInputDevicePlayer3", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer3 = Key.Unbound },
{ "CycleInputDevicePlayer4", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer4 = Key.Unbound },
{ "CycleInputDevicePlayer5", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer5 = Key.Unbound },
{ "CycleInputDevicePlayer6", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer6 = Key.Unbound },
{ "CycleInputDevicePlayer7", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer7 = Key.Unbound },
{ "CycleInputDevicePlayer8", () => viewModel.KeyboardHotkey.CycleInputDevicePlayer8 = Key.Unbound },
{ "CycleInputDeviceHandheld", () => viewModel.KeyboardHotkey.CycleInputDeviceHandheld = Key.Unbound }
};
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
@ -162,6 +171,33 @@ namespace Ryujinx.Ava.UI.Views.Settings
case "TurboMode":
ViewModel.KeyboardHotkey.TurboMode = buttonValue.AsHidType<Key>();
break;
case "CycleInputDevicePlayer1":
ViewModel.KeyboardHotkey.CycleInputDevicePlayer1 = buttonValue.AsHidType<Key>();
break;
case "CycleInputDevicePlayer2":
ViewModel.KeyboardHotkey.CycleInputDevicePlayer2 = buttonValue.AsHidType<Key>();
break;
case "CycleInputDevicePlayer3":
ViewModel.KeyboardHotkey.CycleInputDevicePlayer3 = buttonValue.AsHidType<Key>();
break;
case "CycleInputDevicePlayer4":
ViewModel.KeyboardHotkey.CycleInputDevicePlayer4 = buttonValue.AsHidType<Key>();
break;
case "CycleInputDevicePlayer5":
ViewModel.KeyboardHotkey.CycleInputDevicePlayer5 = buttonValue.AsHidType<Key>();
break;
case "CycleInputDevicePlayer6":
ViewModel.KeyboardHotkey.CycleInputDevicePlayer6 = buttonValue.AsHidType<Key>();
break;
case "CycleInputDevicePlayer7":
ViewModel.KeyboardHotkey.CycleInputDevicePlayer7 = buttonValue.AsHidType<Key>();
break;
case "CycleInputDevicePlayer8":
ViewModel.KeyboardHotkey.CycleInputDevicePlayer8 = buttonValue.AsHidType<Key>();
break;
case "CycleInputDeviceHandheld":
ViewModel.KeyboardHotkey.CycleInputDeviceHandheld = buttonValue.AsHidType<Key>();
break;
}
});
}

View File

@ -1,4 +1,4 @@
<UserControl
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsInputView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -22,9 +22,16 @@
<Panel
Margin="10">
<Grid RowDefinitions="Auto,*,Auto">
<StackPanel>
<!--
Opacity="{Binding PanelOpacityInput}">
IsEnabled="{Binding !EnableConfigGlobal}">
-->
<views:InputView
Grid.Row="0"
Name="InputView" />
</StackPanel>
<StackPanel
Orientation="Vertical"
Grid.Row="2">
@ -34,6 +41,13 @@
<StackPanel
Orientation="Horizontal"
Spacing="10">
<CheckBox
ToolTip.Tip="{ext:Locale UseGlobalInputTooltip}"
MinWidth="0"
IsChecked="{Binding UseGlobalInputConfig}">
<TextBlock
Text="{ext:Locale SettingsTabInputUseGlobalInput}" />
</CheckBox>
<CheckBox
ToolTip.Tip="{ext:Locale DockModeToggleTooltip}"
MinWidth="0"

View File

@ -157,6 +157,32 @@
</ComboBox>
<TextBlock Classes="globalConfigMarker" IsVisible="{Binding IsGameTitleNotNull}" />
</StackPanel>
<Separator Height="1" Margin="0,15,0,15" />
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabUIControllerOverlay}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{ext:Locale SettingsTabUIControllerOverlayGameStartDuration}"
Width="200" />
<NumericUpDown Value="{Binding ControllerOverlayGameStartDuration}"
Minimum="0"
Maximum="30"
Increment="1"
FormatString="0"
ToolTip.Tip="{ext:Locale SettingsTabUIControllerOverlayGameStartDurationTooltip}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{ext:Locale SettingsTabUIControllerOverlayInputCycleDuration}"
Width="200" />
<NumericUpDown Value="{Binding ControllerOverlayInputCycleDuration}"
Minimum="0"
Maximum="30"
Increment="1"
FormatString="0"
ToolTip.Tip="{ext:Locale SettingsTabUIControllerOverlayInputCycleDurationTooltip}" />
</StackPanel>
</StackPanel>
</StackPanel>
</StackPanel>
<Border Grid.Column="1"

View File

@ -1,4 +1,4 @@
<window:StyleableAppWindow xmlns="https://github.com/avaloniaui"
<window:StyleableAppWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Ryujinx.Ava.UI.Helpers"
@ -17,22 +17,234 @@
<window:StyleableAppWindow.DataContext>
<viewModels:CompatibilityViewModel />
</window:StyleableAppWindow.DataContext>
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto" Name="FlushControls">
<Grid RowDefinitions="Auto,Auto,*">
<!-- UI FlushControls -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto,Auto" Name="FlushControls">
<controls:RyujinxLogo
Grid.Column="0"
Margin="15, 0, 7, 0"
ToolTip.Tip="{ext:WindowTitle CompatibilityListTitle, False}"/>
<TextBox Name="SearchBoxFlush" Grid.Column="1" Margin="0, 5, 0, 5" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermarkWithCount}" TextChanged="TextBox_OnTextChanged" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Margin="10, 5, 0, 5">
<TextBlock
Margin="10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
DockPanel.Dock="Right"
Text="{ext:Locale CommonSort}" />
<DropDownButton
Width="150"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{Binding SortName}"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel
Margin="0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel>
<RadioButton
Checked="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameAscending}"
GroupName="Sort"
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
Tag="0" />
<RadioButton
Checked="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameDescending}"
GroupName="Sort"
Tag="1" />
</StackPanel>
<Border
Width="60"
Height="2"
Margin="5"
HorizontalAlignment="Stretch"
BorderBrush="White"
BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton
Checked="Sort_Status_Checked"
Content="{ext:Locale GameListSortStatusDisable}"
GroupName="Order"
IsChecked="{Binding IsSortedByStatus, Mode=OneTime}"
Tag="0" />
<RadioButton
Checked="Sort_Status_Checked"
Content="{ext:Locale GameListSortStatusAscending}"
GroupName="Order"
Tag="1" />
<RadioButton
Checked="Sort_Status_Checked"
Content="{ext:Locale GameListSortStatusDescending}"
GroupName="Order"
Tag="2" />
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</StackPanel>
<CheckBox Grid.Column="3" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
<TextBlock Grid.Column="4" Padding="0, 0, 138, 0" Margin="-10, 0, 18, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</Grid>
<!-- UI NormalControls -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Name="NormalControls">
<TextBox Name="SearchBoxNormal" Grid.Column="0" Margin="15, 0, 0, 5" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermarkWithCount}" TextChanged="TextBox_OnTextChanged" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="10, 0, 5, 5">
<TextBlock
Margin="10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
DockPanel.Dock="Right"
Text="{ext:Locale CommonSort}" />
<DropDownButton
Width="150"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{Binding SortName}"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel
Margin="0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel>
<RadioButton
Checked="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameAscending}"
GroupName="Sort"
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
Tag="0" />
<RadioButton
Checked="Sort_Name_Checked"
Content="{ext:Locale GameListSortStatusNameDescending}"
GroupName="Sort"
Tag="1" />
</StackPanel>
<Border
Width="60"
Height="2"
Margin="5"
HorizontalAlignment="Stretch"
BorderBrush="White"
BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton
Checked="Sort_Status_Checked"
Content="{ext:Locale GameListSortStatusDisable}"
GroupName="Order"
IsChecked="{Binding IsSortedByStatus, Mode=OneTime}"
Tag="0" />
<RadioButton
Checked="Sort_Status_Checked"
Content="{ext:Locale GameListSortStatusAscending}"
GroupName="Order"
Tag="1" />
<RadioButton
Checked="Sort_Status_Checked"
Content="{ext:Locale GameListSortStatusDescending}"
GroupName="Order"
Tag="2" />
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</StackPanel>
<CheckBox Grid.Column="2" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
<TextBlock Grid.Column="3" Padding="0, 0, 138, 0" Margin="-10, 0, 18, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
<TextBlock Grid.Column="3" Padding="0, 0, 1, 0" Margin="-10, 0, 18, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</Grid>
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Name="NormalControls">
<TextBox Name="SearchBoxNormal" Grid.Column="0" Margin="15, 0, 0, 5" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
<CheckBox Grid.Column="1" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
<TextBlock Grid.Column="2" Padding="0, 0, 1, 0" Margin="-10, 0, 18, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
<!-- Description Field Above ScrollViewer -->
<Grid Grid.Row="1" ColumnDefinitions="*,Auto" Margin="10, 5, 10, 5">
<Grid Grid.Column="0">
<Border Classes="listbox-item-style">
<Grid MinWidth="800"
ColumnDefinitions="Auto,Auto,Auto,*,Auto"
Background="Transparent">
<TextBlock Grid.Column="0"
Text="{ext:Locale CompatibilityListGamesAndApplications}"
FontWeight="Bold"
Width="525"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" />
<TextBlock Grid.Column="1"
Text="ID"
FontWeight="Bold"
Width="135"
Padding="7, 0, 0, 0"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" />
<TextBlock Grid.Column="2"
Padding="7, 0"
Text="{ext:Locale CompatibilityListStatus}"
FontWeight="Bold"
Width="100"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="NoWrap" />
<TextBlock Grid.Column="3"
Text="{ext:Locale CompatibilityListDescription}"
FontWeight="Bold"
VerticalAlignment="Center"
HorizontalAlignment="Left"
TextWrapping="WrapWithOverflow" />
</Grid>
<ScrollViewer Grid.Row="1">
</Border>
</Grid>
<Grid Grid.Column="1">
<DropDownButton
Width="80"
Height="35"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="Info"
DockPanel.Dock="Right">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel>
<TextBlock
HorizontalAlignment="Left"
Padding="0,5"
Text="Compatibility verified:" />
<TextBlock
HorizontalAlignment="Left"
Foreground="{Binding IsStringPlayable, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
Text="{Binding PlayableInfoText }" />
<TextBlock
HorizontalAlignment="Left"
Foreground="{Binding IsStringInGame, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
Text="{Binding InGameInfoText }" />
<TextBlock
HorizontalAlignment="Left"
Foreground="{Binding IsStringMenus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
Text="{Binding MenusInfoText }" />
<TextBlock
HorizontalAlignment="Left"
Foreground="{Binding IsStringBoots, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
Text="{Binding BootsInfoText }" />
<TextBlock
HorizontalAlignment="Left"
Foreground="{Binding IsStringNothing, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
Text="{Binding NothingInfoText }" />
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
</Grid>
</Grid>
<!-- List of compatible games -->
<ScrollViewer Grid.Row="2">
<ListBox Margin="12, 0, 13, 0"
Background="Transparent"
ItemsSource="{Binding CurrentEntries}">
@ -60,7 +272,7 @@
<TextBlock Grid.Column="2"
Padding="7, 0"
Text="{Binding LocalizedStatus}"
Width="90"
Width="100"
Background="Transparent"
ToolTip.Tip="{Binding LocalizedStatusDescription}"
Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"

View File

@ -1,5 +1,6 @@
using Avalonia.Controls;
using Ryujinx.Ava.Common.Locale;
using Avalonia.Interactivity;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.ViewModels;
using System.Threading.Tasks;
@ -42,5 +43,28 @@ namespace Ryujinx.Ava.UI.Windows
cvm.Search(searchBox.Text);
}
public void Sort_Name_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton { Tag: string sortStrategy })
{
if (DataContext is not CompatibilityViewModel cvm)
return;
cvm.NameSorting(int.Parse(sortStrategy));
}
}
public void Sort_Status_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton { Tag: string sortStrategy })
{
if (DataContext is not CompatibilityViewModel cvm)
return;
cvm.StatusSorting(int.Parse(sortStrategy));
}
}
}
}

View File

@ -120,15 +120,24 @@
IconSource="Code" />
</ui:NavigationView.MenuItems>
</ui:NavigationView>
<Grid Grid.Row="3"
ColumnDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="10" Spacing="10">
<Button
IsVisible="{Binding !IsGameRunning}"
Content="{ext:Locale UserProfilesDelete}"
Command="{Binding DeleteConfigGame}"
Classes="red"/>
</StackPanel>
<ReversibleStackPanel
Grid.Row="2"
Grid.Column="2"
Margin="10"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
<Button
Classes="accent"
Content="{ext:Locale SettingsButtonSave}"
Command="{Binding SaveUserConfig}" />
<Button
@ -136,14 +145,9 @@
Content="{ext:Locale SettingsButtonClose}"
Command="{Binding CancelButton}" />
<Button
IsVisible="{Binding IsGameRunning}"
Content="{ext:Locale SettingsButtonApply}"
Command="{Binding ApplyButton}" />
<Button
IsVisible="{Binding !IsGameRunning}"
Content="{ext:Locale UserProfilesDelete}"
Command="{Binding DeleteConfigGame}"
Classes="red"/>
</ReversibleStackPanel>
</Grid>
</Grid>
</window:StyleableAppWindow>

View File

@ -28,6 +28,8 @@ namespace Ryujinx.Ava.UI.Windows
ViewModel.CloseWindow += Close;
ViewModel.SaveSettingsEvent += SaveSettings;
ViewModel.LocalGlobalInputSwitchEvent += ToggleLocalGlobalInput;
InitializeComponent();
Load();
}
@ -37,6 +39,11 @@ namespace Ryujinx.Ava.UI.Windows
InputPage.InputView?.SaveCurrentProfile();
}
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
{
InputPage.InputView?.ToggleLocalGlobalInput(enableConfigGlobal);
}
private void Load()
{
Pages.Children.Clear();
@ -90,6 +97,7 @@ namespace Ryujinx.Ava.UI.Windows
protected override void OnClosing(WindowClosingEventArgs e)
{
Program.UseExtraConfig = false;
InputPage.Dispose(); // You need to unload the gamepad settings, otherwise the controls will be blocked
base.OnClosing(e);
}

View File

@ -10,6 +10,7 @@
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:main="clr-namespace:Ryujinx.Ava.UI.Views.Main"
xmlns:viewsMisc="clr-namespace:Ryujinx.Ava.UI.Views.Misc"
xmlns:overlayControls="clr-namespace:Ryujinx.Ava.UI.Controls"
Cursor="{Binding Cursor}"
Title="{Binding Title}"
WindowState="{Binding WindowState}"
@ -178,5 +179,7 @@
Name="StatusBarView"
Grid.Row="2" />
</Grid>
</Grid>
</window:StyleableAppWindow>

View File

@ -213,13 +213,13 @@ namespace Ryujinx.Ava.UI.Windows
}
}
public void Application_Opened(object sender, ApplicationOpenedEventArgs args)
public async void Application_Opened(object sender, ApplicationOpenedEventArgs args)
{
if (args.Application != null)
{
ViewModel.SelectedIcon = args.Application.Icon;
ViewModel.LoadApplication(args.Application).Wait();
await ViewModel.LoadApplication(args.Application);
}
args.Handled = true;