Compare commits

...

69 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
72 changed files with 3684 additions and 1321 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 }}
@ -92,16 +44,6 @@ jobs:
- name: Overwrite csc problem matcher
run: echo "::add-matcher::.github/csc.json"
- 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: Get version info
id: version_info
run: |
@ -204,33 +146,6 @@ jobs:
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
@ -290,28 +205,15 @@ jobs:
./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:
- tag
- macos_release
- release
steps:
- uses: actions/checkout@v4
- name: Get version info
id: version_info
run: |
@ -330,10 +232,18 @@ jobs:
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](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})**"
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"
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

@ -14,38 +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: 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 }}
@ -169,30 +137,6 @@ jobs:
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
@ -250,25 +194,10 @@ jobs:
./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:
- tag
- macos_release
- release
steps:
@ -299,3 +228,7 @@ jobs:
- 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

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

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

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

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

@ -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,20 +0,0 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Common.Models.GitLab
{
public class GitLabReleaseAssetJsonResponse
{
[JsonPropertyName("links")]
public GitLabReleaseAssetLinkJsonResponse[] Links { get; set; }
public class GitLabReleaseAssetLinkJsonResponse
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("name")]
public string AssetName { get; set; }
[JsonPropertyName("url")]
public string Url { get; set; }
}
}
}

View File

@ -1,19 +0,0 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Common.Models.GitLab
{
public class GitLabReleasesJsonResponse
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("tag_name")]
public string TagName { get; set; }
[JsonPropertyName("assets")]
public GitLabReleaseAssetJsonResponse Assets { get; set; }
}
[JsonSerializable(typeof(GitLabReleasesJsonResponse), GenerationMode = JsonSourceGenerationMode.Metadata)]
public partial class GitLabReleasesJsonSerializerContext : JsonSerializerContext;
}

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();
NpadManager.Initialize(Device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
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

@ -1,190 +0,0 @@
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models.Github;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Systems
{
internal static partial class Updater
{
private static GitHubReleaseChannels.Channel? _currentGitHubReleaseChannel;
private static async Task<Optional<(Version Current, Version Incoming)>> CheckGitHubVersionAsync(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 from GitHub.");
// Get latest version number from GitHub API
try
{
using HttpClient jsonClient = ConstructHttpClient();
if (_currentGitHubReleaseChannel == null)
{
GitHubReleaseChannels releaseChannels = await GitHubReleaseChannels.GetAsync(jsonClient);
_currentGitHubReleaseChannel = ReleaseInformation.IsCanaryBuild
? releaseChannels.Canary
: releaseChannels.Stable;
Logger.Info?.Print(LogClass.Application, $"Loaded GitHub release channel for '{(ReleaseInformation.IsCanaryBuild ? "canary" : "stable")}'");
_changelogUrlFormat = _currentGitHubReleaseChannel.Value.UrlFormat;
}
string fetchedJson = await jsonClient.GetStringAsync(_currentGitHubReleaseChannel.Value.GetLatestReleaseApiUrl());
GithubReleasesJsonResponse fetched = JsonHelper.Deserialize(fetchedJson, _ghSerializerContext.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(_changelogUrlFormat.Format(currentVersion));
}
}
Logger.Info?.Print(LogClass.Application, "Up to date.");
_running = false;
return default;
}
break;
}
}
// If build not done, assume no new update is 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(_changelogUrlFormat.Format(currentVersion));
}
}
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 readonly struct GitHubReleaseChannels
{
public static async Task<GitHubReleaseChannels> GetAsync(HttpClient httpClient)
{
ReleaseChannelPair releaseChannelPair = await httpClient.GetFromJsonAsync("https://ryujinx.app/api/release-channels", ReleaseChannelPairContext.Default.ReleaseChannelPair);
return new GitHubReleaseChannels(releaseChannelPair);
}
internal GitHubReleaseChannels(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 string UrlFormat => $"https://github.com/{ToString()}/releases/{{0}}";
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

@ -1,25 +1,31 @@
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models.GitLab;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.Systems.Update.Client;
using Ryujinx.Systems.Update.Common;
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Systems
{
internal static partial class Updater
{
private static GitLabReleaseChannels.ChannelType _currentGitLabReleaseChannel;
private static VersionResponse _versionResponse;
private static async Task<Optional<(Version Current, Version Incoming)>> CheckGitLabVersionAsync(bool showVersionUpToDate = false)
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))
{
@ -35,38 +41,31 @@ namespace Ryujinx.Ava.Systems
return default;
}
Logger.Info?.Print(LogClass.Application, "Checking for updates from https://git.ryujinx.app.");
using UpdateClient updateClient = CreateUpdateClient();
// Get latest version number from GitLab API
using HttpClient jsonClient = ConstructHttpClient();
// GitLab instance is located in Ukraine. Connection times will vary across the world.
jsonClient.Timeout = TimeSpan.FromSeconds(10);
if (_currentGitLabReleaseChannel == null)
try
{
GitLabReleaseChannels releaseChannels = await GitLabReleaseChannels.GetAsync(jsonClient);
_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}");
_currentGitLabReleaseChannel = ReleaseInformation.IsCanaryBuild
? releaseChannels.Canary
: releaseChannels.Stable;
Logger.Info?.Print(LogClass.Application, $"Loaded GitLab release channel for '{(ReleaseInformation.IsCanaryBuild ? "canary" : "stable")}'");
_changelogUrlFormat = _currentGitLabReleaseChannel.UrlFormat;
_running = false;
return default;
}
string fetchedJson = await jsonClient.GetStringAsync(_currentGitLabReleaseChannel.GetLatestReleaseApiUrl());
GitLabReleasesJsonResponse fetched = JsonHelper.Deserialize(fetchedJson, _glSerializerContext.GitLabReleasesJsonResponse);
_buildVer = fetched.TagName;
_buildUrl = fetched.Assets.Links
.FirstOrDefault(link =>
link.AssetName.StartsWith("ryujinx") && link.AssetName.EndsWith(_platformExt)
)?.Url;
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 (_buildUrl is null)
if (_versionResponse.ArtifactUrl is null or "")
{
if (showVersionUpToDate)
{
@ -76,7 +75,7 @@ namespace Ryujinx.Ava.Systems
if (userResult is UserResult.Ok)
{
OpenHelper.OpenUrl(_changelogUrlFormat.Format(currentVersion));
OpenHelper.OpenUrl(_versionResponse.ReleaseUrlFormat.Format(currentVersion));
}
}
@ -88,13 +87,13 @@ namespace Ryujinx.Ava.Systems
}
if (!Version.TryParse(_buildVer, out Version newVersion))
if (!Version.TryParse(_versionResponse.Version, out Version newVersion))
{
Logger.Error?.Print(LogClass.Application,
$"Failed to convert the received {RyujinxApp.FullAppName} version from GitLab!");
$"Failed to convert the received {RyujinxApp.FullAppName} version from the update server!");
await ContentDialogHelper.CreateWarningDialog(
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedServerMessage],
LocaleManager.Instance[LocaleKeys.DialogUpdaterCancelUpdateMessage]);
_running = false;
@ -104,35 +103,5 @@ namespace Ryujinx.Ava.Systems
return (currentVersion, newVersion);
}
[JsonSerializable(typeof(GitLabReleaseChannels))]
partial class GitLabReleaseChannelPairContext : JsonSerializerContext;
public class GitLabReleaseChannels
{
public static async Task<GitLabReleaseChannels> GetAsync(HttpClient httpClient)
=> await httpClient.GetFromJsonAsync(
"https://git.ryujinx.app/ryubing/ryujinx/-/snippets/1/raw/main/meta.json",
GitLabReleaseChannelPairContext.Default.GitLabReleaseChannels);
[JsonPropertyName("stable")] public ChannelType Stable { get; set; }
[JsonPropertyName("canary")] public ChannelType Canary { get; set; }
public class ChannelType
{
[JsonPropertyName("id")] public long Id { get; set; }
[JsonPropertyName("group")] public string Group { get; set; }
[JsonPropertyName("project")] public string Project { get; set; }
public string UrlFormat => $"https://git.ryujinx.app/{ToString()}/-/releases/{{0}}";
public override string ToString() => $"{Group}/{Project}";
public string GetLatestReleaseApiUrl() =>
$"https://git.ryujinx.app/api/v4/projects/{Id}/releases/permalink/latest";
}
}
}
}

View File

@ -5,14 +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.Common.Models.GitLab;
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;
@ -32,48 +29,17 @@ namespace Ryujinx.Ava.Systems
{
internal static partial class Updater
{
private static readonly GithubReleasesJsonSerializerContext _ghSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
private static readonly GitLabReleasesJsonSerializerContext _glSerializerContext = 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 = [];
private static string _changelogUrlFormat = null;
public static async Task<Optional<(Version, Version)>> CheckVersionAsync(bool showVersionUpToDate = false)
{
Optional<(Version, Version)> versionTuple;
try
{
versionTuple = await CheckGitLabVersionAsync(showVersionUpToDate);
}
catch (Exception e)
{
Logger.Error?.PrintMsg(LogClass.Application, "Update checking from GitLab failed; falling back to GitHub.");
Logger.Error?.PrintMsg(LogClass.Application, e.Message);
versionTuple = await CheckGitHubVersionAsync(showVersionUpToDate);
}
return versionTuple;
}
public static async Task BeginUpdateAsync(bool showVersionUpToDate = false)
{
if (_running)
@ -100,7 +66,7 @@ namespace Ryujinx.Ava.Systems
if (userResult is UserResult.Ok)
{
OpenHelper.OpenUrl(_changelogUrlFormat.Format(currentVersion));
OpenHelper.OpenUrl(_versionResponse.ReleaseUrlFormat.Format(currentVersion));
}
}
@ -120,7 +86,7 @@ namespace Ryujinx.Ava.Systems
// 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(_buildUrl), HttpCompletionOption.ResponseHeadersRead);
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_versionResponse.ArtifactUrl), HttpCompletionOption.ResponseHeadersRead);
_buildSize = message.Content.Headers.ContentRange.Length.Value;
}
@ -150,7 +116,7 @@ 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:
@ -168,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)
{
Config = inputConfig ?? ConfigurationState.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
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 = [];
newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value);
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);
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
}
public void NotifyChange(string property)
{
OnPropertyChanged(property);
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 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,7 +56,27 @@
SelectedIndex="{Binding PreferredGpuIndex}"
ItemsSource="{Binding AvailableGpus}"/>
</StackPanel>
</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}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
@ -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">
<views:InputView
Grid.Row="0"
Name="InputView" />
<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,65 +17,277 @@
<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>
</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>
<ScrollViewer Grid.Row="1">
<ListBox Margin="12, 0, 13, 0"
Background="Transparent"
ItemsSource="{Binding CurrentEntries}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type systems:CompatibilityEntry}">
<Grid MinWidth="800"
Margin="10"
ColumnDefinitions="Auto,Auto,Auto,*"
Background="Transparent"
ToolTip.Tip="{Binding LocalizedLastUpdated}">
<TextBlock Grid.Column="0"
Text="{Binding GameName}"
Width="525"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" />
<TextBlock Grid.Column="1"
Width="135"
Padding="7, 0, 0, 0"
FontFamily="{StaticResource JetBrainsMono}"
Text="{Binding FormattedTitleId}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" />
<TextBlock Grid.Column="2"
Padding="7, 0"
Text="{Binding LocalizedStatus}"
Width="90"
Background="Transparent"
ToolTip.Tip="{Binding LocalizedStatusDescription}"
Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="NoWrap" />
<TextBlock Grid.Column="3"
Text="{Binding FormattedIssueLabels}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
TextWrapping="WrapWithOverflow" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- List of compatible games -->
<ScrollViewer Grid.Row="2">
<ListBox Margin="12, 0, 13, 0"
Background="Transparent"
ItemsSource="{Binding CurrentEntries}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type systems:CompatibilityEntry}">
<Grid MinWidth="800"
Margin="10"
ColumnDefinitions="Auto,Auto,Auto,*"
Background="Transparent"
ToolTip.Tip="{Binding LocalizedLastUpdated}">
<TextBlock Grid.Column="0"
Text="{Binding GameName}"
Width="525"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" />
<TextBlock Grid.Column="1"
Width="135"
Padding="7, 0, 0, 0"
FontFamily="{StaticResource JetBrainsMono}"
Text="{Binding FormattedTitleId}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" />
<TextBlock Grid.Column="2"
Padding="7, 0"
Text="{Binding LocalizedStatus}"
Width="100"
Background="Transparent"
ToolTip.Tip="{Binding LocalizedStatusDescription}"
Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="NoWrap" />
<TextBlock Grid.Column="3"
Text="{Binding FormattedIssueLabels}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
TextWrapping="WrapWithOverflow" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
<Grid></Grid>
</Grid>

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,30 +120,34 @@
IconSource="Code" />
</ui:NavigationView.MenuItems>
</ui:NavigationView>
<ReversibleStackPanel
Grid.Row="2"
Margin="10"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{x:Static helper:RunningPlatform.IsMacOS}">
<Button
Content="{ext:Locale SettingsButtonSave}"
Command="{Binding SaveUserConfig}" />
<Button
HotKey="Escape"
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.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.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
HotKey="Escape"
Content="{ext:Locale SettingsButtonClose}"
Command="{Binding CancelButton}" />
<Button
Content="{ext:Locale SettingsButtonApply}"
Command="{Binding ApplyButton}" />
</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;