Compare commits

..

26 Commits

Author SHA1 Message Date
LotP
e777e3f93b fix-tests (#140)
- downgrade unicorn to last working version
- update to new cp reg struct system
- remove nonexistent register (used to silently continue)
- fix partial unmap tests
    - InitializeSignalHandler() was moved out of the translator (almost 2.5 years ago), but the test code was never updated to manually call the function as it was changed to do in the real cpu context, so the tests just started failing.
    - by manually initializing the handler we no longer cause tests to fail.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/140
2026-06-15 02:25:18 +00:00
KeatonTheBot
7b19e041cb Linux: Fix remaining file/folder picker issues (#24)
I fixed the remaining Linux file picker issues after testing on Steam Deck. User profile images, mod manager, title manager, and DLC directory were still using the old file picker methods and not the helper methods. I could only apply the helper method to user profiles, but I came up with a workaround for the others.

The reason for the draft PR: I'd ideally like to fix the other three at the helper level, so maybe @greem can help with that since since he wrote the initial implementation.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/24
2026-06-15 01:44:24 +00:00
AllieNeedsSleep
e7a3c94b9c Discord RPC grammatical changes (#139)
Adjusted wording used in Discord RPC for clarity.

Co-authored-by: Allie <barstaxjolster@gmail.com>
Co-authored-by: sh0inx <randomgirlisweird@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/139
2026-06-14 22:13:59 +00:00
_Neo_
223f20868a UI: File Menu → General Improvements (#127)
Ayyyy, welcome to the UI: File Menu → General Improvements PR!

Wooo, we progressing smoothly!

This PR introduces small visual and "feature" improvements to the `File` menu.

### LOCALISATION:
* **Fractured:** More locales:
    * `Dialog_ContentLoading.json` - content loading dialogs (Updates/DLC)
    * `Dialog_FileTypeAssociations` - file association dialogs
    * `Dialog_FileMenu` - File menu dialog strings (complements `MenuBar_File.json`)
* **Added:** Additional entires to `Error.json`
* **Populated:** `MenuBar_File.json`

### FILE MENU:
* **Added:** Keyboard shortcuts to `Load Application` and `Load Unpacked Game`
    * Cmd + O/Ctrl + O and Cmd + Shift + O/Ctrl + Shift + O for macOS and other OS, respectively.
    * While many users rely on autoloaded game directories, manually opening content remains a common workflow (for those that don't rely on autoload directories).
* **Merged:** `Load Title Updates` and `Load DLC` → `Load Updates/DLC`
    * Both actions follow the same workflow: selecting one or more directories and allowing Ryujinx to load the content.
    * To reduce redundancy, they have been consolidated into a single menu item that loads both Updates and DLC simultaneously, mirroring the behavior of the existing autoload functionality.
    * As part of this change, Title Updates has been simplified to Updates for consistency with the rest of the UI. The remaining reference in the Game List context menu has also been updated from `Manage Title Updates` to `Manage Updates`.
* **Added/Updated:** File picker titles for content loading actions
    * `Load Application`: Select a Switch application file to load
    * `Load Unpacked Game`: Select a folder containing an unpacked Switch application to load
    * `Load Updates/DLC`: Select one or more folders to bulk load updates and DLC from
* **Improved:** `Associate File Types` and `Remove File Type Associations` (initially moved to the `File` menu in #42)
    * These options were previously nested under `Manage File Types` and exposed as `Install File Types` and `Uninstall File Types`. The submenu added unnecessary navigation, while the action names did not clearly communicate their purpose.
    * The two actions have been replaced with a single dynamic menu item, whose displayed and performed action updates based on the current association state. The respective icons have been added as well (imported namespace Projektanker.Icons to allow for dynamic switching):
        * Link → `Associate File Types`
        * Link-Slash →`Remove File Type Associations`
    * A tooltip has also been added to clarify the action being performed.
    * This option is only usable when a game is not running (why associate file types when running a game? Play the game!)
    * **Note:** These options are only available on Windows and Linux. macOS already provides robust per‑file “Open With” handling, so a custom association system isn’t necessary. Support can be added later if needed, but current macOS limitations in Ryujinx prevent certain behaviors; these will be addressed in future PRs.
* **Updated:** Menu Icons
    * Several icons have been updated to better reflect their associated actions and improve consistency throughout the menu:
        * `Load Updates/DLC...` now uses a single Inbox Tray icon instead of separate Update and DLC icons.
        * `Open Ryujinx Folder`, `Open Logs Folder`, and `Open Screenshots Folder` now share the same folder icon, as all three actions ultimately open a folder/directory.
        * `Exit` is now an Exit icon (arrow-right-from-bracket) instead of a Power button.

### OTHER:
* **Improved:** `Load Updates/DLC` dialog messages
    * Existing dialog messages were longer than necessary and included terms such as "missing" updates and "new" DLC.
    * These messages have been simplified and standardized:
        * Updates Added: {0} or Updates Removed: {0}
        * DLC Added: {0} or DLC Removed: {0}

_If there are any features or changes that you wish to be implemented, please comment down below and I'll be happy to accommodate!_

A GIGANTIC, ENORMOUSE HUUUUUUGEE thank you to @Babib3l for testing this and ensuring the commands work! WOOOOO!!!

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/127
2026-06-14 19:47:20 +00:00
_Neo_
8fe1a9c672 UI: Options Menu (Part 1 of 2) → General Improvements (#133)
Ayyyy, welcome to the UI: Options Menu → General Improvements PR!

We cruisin'!

This is the first PR of a small series aimed at improving the `Options` menu and related submenus.

This initial PR focuses on smaller visual and navigational improvements to the main `Options` menu. As this is part of a progressive series (two PRs _for now_), further bug fixes and refinements may be present in Part 2. Additionally, as the `View` menu is related to some of the changes presents in this PR, it has also received minor adjustments (see details below).

### LOCALISATION
* **Fractured:**
    * `MenuBar_Options.json` - `Options` menu locales
    * `MenuBar_View.json` - `View` menu locales
    * `Settings_Interface.json` - Interface `Settings` tab locales (UI-only)
* **Added:** Missing "e" in Italian translation of `User Profiles` (thank you BOBBIJDJ for pointing it out!)

**NOTE:** `MenuBar_View.json` and `Settings_Interface.json` were not populated in this PR.

### OPTIONS MENU
* **Moved:** `Toggle Fullscreen` to the `View` menu.
    * This option affects the current window/view state, so it belongs under `View` rather than `Options`.
* **Moved:** `Show Console` checkbox to the `View` menu.
    * Even though it was previously under `Options`, this is a real-time visibility toggle. It affects Ryujinx "immediately" (shows/hides the console window, and as immediately as you can get with our current implementation), so it fits better alongside other current view controls like `Toggle Fullscreen` and the `Window Size` presets.
* **Moved:** `Start Games in Fullscreen` and `Start Games Without UI` (renamed to "Hide UI on Game Start" for better flow) from the `Options` menu to the `Interface` tab in `Settings`.
    * These are persistent launch preferences rather than actions that can affect just the current session. Settings is the semantically correct location for them. They now sit alongside other launch-related options such as `Confirm Game Shutdown`.
    * Their respective locales were transferred to the newly created `Settings_Interface.json`.
* **Renamed:** `Change Language` → `Language`
    * Made consistent with the shorter style used by other items in this menu in previous cleanups.
* **Modified:** Menu item order (see images below).

### VIEW MENU
The `View` menu received only light adjustments in this PR (mainly padding/margins to better fit the newly added `Show Console` checkbox ). A dedicated _View Menu_ follow-up PR is planned to reorganise it further and add new options. Furthermore, _Options Menu (Part 2 of 2)_ will follow shortly as well.

_If there are any features or changes that you wish to be implemented, please comment down below and I'll be happy to accommodate!_

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/133
2026-06-14 19:21:25 +00:00
Renovate Bot
aad1a71f68 Update dependency Sep to 0.15.0 (#137)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [Sep](https://github.com/nietras/Sep) | `0.14.1` → `0.15.0` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Sep/0.15.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Sep/0.14.1/0.15.0?slim=true) |

---

### Release Notes

<details>
<summary>nietras/Sep (Sep)</summary>

### [`v0.15.0`](https://github.com/nietras/Sep/releases/tag/v0.15.0): 0.15.0

#### What's Changed

Minor breaking changes to API parameter names.

- Bump actions/setup-dotnet from 5.2.0 to 5.3.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;545](https://github.com/nietras/Sep/pull/545)
- Bump github/codeql-action from 4.35.3 to 4.36.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;544](https://github.com/nietras/Sep/pull/544)
- Bump codecov/codecov-action from 6.0.0 to 6.0.1 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;547](https://github.com/nietras/Sep/pull/547)
- Bump coverlet.collector from 10.0.0 to 10.0.1 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;549](https://github.com/nietras/Sep/pull/549)
- Bump actions/dependency-review-action from 4.9.0 to 5.0.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;548](https://github.com/nietras/Sep/pull/548)
- Bump step-security/harden-runner from 2.19.1 to 2.19.4 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;546](https://github.com/nietras/Sep/pull/546)
- Bump MSTest from 4.2.2 to 4.2.3 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;550](https://github.com/nietras/Sep/pull/550)
- Fix typos: `degreeOfParallism` parameter name and missing quote in exception message by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;551](https://github.com/nietras/Sep/pull/551)
- Remove unnessary `unsafe` usage by [@&#8203;nietras](https://github.com/nietras) in [#&#8203;552](https://github.com/nietras/Sep/pull/552)
- Address and remove TODOs by [@&#8203;nietras](https://github.com/nietras) in [#&#8203;554](https://github.com/nietras/Sep/pull/554)
- Fix Reader Spec forwarding, SepWriter dispose and README nits, expand tests by [@&#8203;nietras](https://github.com/nietras) in [#&#8203;555](https://github.com/nietras/Sep/pull/555)
- Bump github/codeql-action from 4.36.0 to 4.36.2 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;558](https://github.com/nietras/Sep/pull/558)
- Bump actions/checkout from 6.0.2 to 6.0.3 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;556](https://github.com/nietras/Sep/pull/556)
- Bump codecov/codecov-action from 6.0.1 to 7.0.0 by [@&#8203;dependabot](https://github.com/dependabot)\[bot] in [#&#8203;557](https://github.com/nietras/Sep/pull/557)

**Full Changelog**: <https://github.com/nietras/Sep/compare/v0.14.1...v0.15.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNzguMCIsInVwZGF0ZWRJblZlciI6IjQzLjE3OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/137
2026-06-14 18:59:33 +00:00
Renovate Bot
25120667f1 Update dependency Microsoft.IdentityModel.JsonWebTokens to 8.19.1 (#136)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) | `8.18.0` → `8.19.1` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Microsoft.IdentityModel.JsonWebTokens/8.19.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Microsoft.IdentityModel.JsonWebTokens/8.18.0/8.19.1?slim=true) |

---

### Release Notes

<details>
<summary>AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet (Microsoft.IdentityModel.JsonWebTokens)</summary>

### [`v8.19.1`](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/HEAD/CHANGELOG.md#8191)

[Compare Source](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/8.19.0...8.19.1)

\====

#### Bug Fixes

- Update `JwtSecurityTokenHandler` for `IssuerSigningKeyResolverUsingConfiguration` to take priority over `IssuerSigningKeyResolver`, matching the documented contract and the correct behavior already present in `JsonWebTokenHandler`. See [PR #&#8203;3519](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3519).

### [`v8.19.0`](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/HEAD/CHANGELOG.md#8190)

[Compare Source](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/8.18.0...8.19.0)

\====

#### New Features

- Add ML-DSA (FIPS 204) post-quantum signature support. See [PR #&#8203;3479](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3479).
- Cache custom crypto providers in CryptoProviderFactory. See [PR #&#8203;3489](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3489).

#### Bug Fixes

- Disable automatic redirects on default HttpClient for JKU retrieval. See [PR #&#8203;3494](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3494).
- Adjust rented buffer handling in claim set parsing. See [PR #&#8203;3493](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3493).
- Tidy null handling in SAML conditions validation. See [PR #&#8203;3491](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3491).
- Improve validation of `jku` claim. See [PR #&#8203;3481](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3481).
- Limit telemetry algorithm dimension cardinality. See [PR #&#8203;3490](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3490).
- Add defensive copy of collections in ValidationParameters. See [PR #&#8203;3492](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3492).
- Update TokenValidationParameter copy constructor to make a deep copy. See [PR #&#8203;3488](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3488).
- Update to fail-closed when replay protection isn't configured and other DPoP hardening. See [PR #&#8203;3505](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3505).
- Apply RFC 3986 section 6.2.2 normalization to DPoP `htu` comparison. See [PR #&#8203;3509](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3509).

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNzguMCIsInVwZGF0ZWRJblZlciI6IjQzLjE3OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/136
2026-06-14 18:53:00 +00:00
Renovate Bot
9061c3a2b3 Update dependency Ryujinx.Audio.OpenAL to 1.25.2 (#135)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| Ryujinx.Audio.OpenAL | `1.25.1` → `1.25.2` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Ryujinx.Audio.OpenAL/1.25.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Ryujinx.Audio.OpenAL/1.25.1/1.25.2?slim=true) |

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNzguMCIsInVwZGF0ZWRJblZlciI6IjQzLjE3OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/135
2026-06-14 18:52:21 +00:00
awesomeangotti
963f024838 Discord Rich Presence: Miitopia (#129)
Hello and welcome to my PR that adds discord rich presence stuff from play reports generated by Miitopia.

The following items are tracked and displayed:
Main menu (for booting game)
MII Selector (from main menu)
Region (Game stage based)
Gold (Only updates at start of game play due to lack of play reports generated after loading into world)

As a bonus, I will be exporting the title icon for discord rich presence images and providing the title ID so it can be added to rich presence images. (See bottom of my message)

Examples of current rich presence messages:

At Greenhorne (Starting area)
![image](/attachments/663acf62-2149-4e69-a68d-94c107876182)

After defeating final boss
![image](/attachments/52363124-742b-4214-8b69-f8043ae382bc)

Rich presence image and titleID:

titleID: 01003da010e8a000

![01003da010e8a000](/attachments/952d9218-9b99-4d55-929e-d3c48dcf87a4)

Co-authored-by: Awesomeangotti <143439211+Awesomeangotti@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/129
2026-06-14 17:32:12 +00:00
shinyoyo
c263c211bd Updated Simplified Chinese Translation (#134)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/134
2026-06-14 02:02:21 +00:00
sh0inx
77c9a50f59 Update README.md 2026-06-04 09:04:55 +00:00
Renovate Bot
a9ce288632 Update dependency SharpCompress to 0.49.1 (#123)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [SharpCompress](https://github.com/adamhathcock/sharpcompress) | `0.48.1` → `0.49.1` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/SharpCompress/0.49.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/SharpCompress/0.48.1/0.49.1?slim=true) |

---

### Release Notes

<details>
<summary>adamhathcock/sharpcompress (SharpCompress)</summary>

### [`v0.49.1`](https://github.com/adamhathcock/sharpcompress/releases/tag/0.49.1): - More fixes

[Compare Source](https://github.com/adamhathcock/sharpcompress/compare/0.49.0...0.49.1)

#### What's Changed

- Close writable entry streams during async archive disposal by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1338](https://github.com/adamhathcock/sharpcompress/pull/1338)
- Restore `WriteToDirectoryAsync` progress callbacks for solid 7z archives by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1340](https://github.com/adamhathcock/sharpcompress/pull/1340)
- Try to fix global.json to avoid churn in locks by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1341](https://github.com/adamhathcock/sharpcompress/pull/1341)
- Fix tar archive enumeration after fully reading entry streams by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1342](https://github.com/adamhathcock/sharpcompress/pull/1342)

**Full Changelog**: <https://github.com/adamhathcock/sharpcompress/compare/0.49.0...0.49.1>

### [`v0.49.0`](https://github.com/adamhathcock/sharpcompress/releases/tag/0.49.0): - Write Async fixes and more.

[Compare Source](https://github.com/adamhathcock/sharpcompress/compare/0.48.1...0.49.0)

This should contain a lot of write async fixes and some breaking API changes that fix previous broke `net48` usage

#### What's Changed

- Rename IWriteableArchiveFactory.cs to IWritableArchiveFactory.cs by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1244](https://github.com/adamhathcock/sharpcompress/pull/1244)
- Some API clean up from GPT 5.4 by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1243](https://github.com/adamhathcock/sharpcompress/pull/1243)
- Release to master by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1267](https://github.com/adamhathcock/sharpcompress/pull/1267)
- Fix three BLAKE2sp correctness bugs and eliminate allocations in hot path by [@&#8203;coderb](https://github.com/coderb) in [#&#8203;1266](https://github.com/adamhathcock/sharpcompress/pull/1266)
- Corrected async examples. by [@&#8203;dlemstra](https://github.com/dlemstra) in [#&#8203;1277](https://github.com/adamhathcock/sharpcompress/pull/1277)
- Fix setting invalid access time fails extraction by [@&#8203;aromaa](https://github.com/aromaa) in [#&#8203;1279](https://github.com/adamhathcock/sharpcompress/pull/1279)
- Fix incorrect code examples in docs for sync/async usage by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1280](https://github.com/adamhathcock/sharpcompress/pull/1280)
- Replace APPNOTE.TXT contents with reference link note by [@&#8203;puk06](https://github.com/puk06) in [#&#8203;1286](https://github.com/adamhathcock/sharpcompress/pull/1286)
- Release to Master by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1274](https://github.com/adamhathcock/sharpcompress/pull/1274)
- update docs for tar gap analysis and XZ usage by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1288](https://github.com/adamhathcock/sharpcompress/pull/1288)
- Add a PooledMemoryStream to avoid allocating by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1275](https://github.com/adamhathcock/sharpcompress/pull/1275)
- fix: Change LeaveStreamOpen default from true to false by [@&#8203;puk06](https://github.com/puk06) in [#&#8203;1293](https://github.com/adamhathcock/sharpcompress/pull/1293)
- Fix usage of ReaderOptions and pre-defined values by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1295](https://github.com/adamhathcock/sharpcompress/pull/1295)
- Enforce seekable, readable and writable on streams by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1297](https://github.com/adamhathcock/sharpcompress/pull/1297)
- Add ArchiveInformation record for consolidated archive detection and capability inspection by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1299](https://github.com/adamhathcock/sharpcompress/pull/1299)
- merge release to master by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1314](https://github.com/adamhathcock/sharpcompress/pull/1314)
- Some clean up and test clean up by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1321](https://github.com/adamhathcock/sharpcompress/pull/1321)
- Finish Write Async by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1323](https://github.com/adamhathcock/sharpcompress/pull/1323)
- More complete Tar implementation: USTAR, PAX, etc. by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1289](https://github.com/adamhathcock/sharpcompress/pull/1289)
- Add Polysharp and adjustments that do not break legacy frameworks by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1330](https://github.com/adamhathcock/sharpcompress/pull/1330)
- Fix null `IVolume.FileName` for single-volume file-based archives by [@&#8203;Copilot](https://github.com/Copilot) in [#&#8203;1333](https://github.com/adamhathcock/sharpcompress/pull/1333)
- Add skills by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1332](https://github.com/adamhathcock/sharpcompress/pull/1332)
- add AOT smoke and missing tests by [@&#8203;adamhathcock](https://github.com/adamhathcock) in [#&#8203;1334](https://github.com/adamhathcock/sharpcompress/pull/1334)

#### New Contributors

- [@&#8203;dlemstra](https://github.com/dlemstra) made their first contribution in [#&#8203;1277](https://github.com/adamhathcock/sharpcompress/pull/1277)
- [@&#8203;aromaa](https://github.com/aromaa) made their first contribution in [#&#8203;1279](https://github.com/adamhathcock/sharpcompress/pull/1279)
- [@&#8203;puk06](https://github.com/puk06) made their first contribution in [#&#8203;1286](https://github.com/adamhathcock/sharpcompress/pull/1286)

**Full Changelog**: <https://github.com/adamhathcock/sharpcompress/compare/0.48.1...0.49.0>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNzguMCIsInVwZGF0ZWRJblZlciI6IjQzLjE3OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/123
2026-05-31 10:51:01 +00:00
Renovate Bot
9b88fee78f Update avalonia monorepo to 11.3.17 (#122)
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [Avalonia](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.15` → `11.3.17` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia/11.3.17?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia/11.3.15/11.3.17?slim=true) |
| [Avalonia.Desktop](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.15` → `11.3.17` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Desktop/11.3.17?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Desktop/11.3.15/11.3.17?slim=true) |
| [Avalonia.Diagnostics](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.15` → `11.3.17` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Diagnostics/11.3.17?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Diagnostics/11.3.15/11.3.17?slim=true) |
| [Avalonia.Markup.Xaml.Loader](https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link) ([source](https://github.com/AvaloniaUI/Avalonia)) | `11.3.15` → `11.3.17` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Markup.Xaml.Loader/11.3.17?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Markup.Xaml.Loader/11.3.15/11.3.17?slim=true) |

---

### Release Notes

<details>
<summary>AvaloniaUI/Avalonia (Avalonia)</summary>

### [`v11.3.17`](https://github.com/AvaloniaUI/Avalonia/releases/tag/11.3.17)

[Compare Source](https://github.com/AvaloniaUI/Avalonia/compare/11.3.16...11.3.17)

##### What's Changed

##### Performance

- XAML – Improve type caching in the XAML compiler by [@&#8203;MrJul](https://github.com/MrJul) in [#&#8203;21408](https://github.com/AvaloniaUI/Avalonia/pull/21408)

**Full Changelog**: <https://github.com/AvaloniaUI/Avalonia/compare/11.3.16...11.3.17>

### [`v11.3.16`](https://github.com/AvaloniaUI/Avalonia/releases/tag/11.3.16)

[Compare Source](https://github.com/AvaloniaUI/Avalonia/compare/11.3.15...11.3.16)

##### What's Changed

- Core – Align Touch/Pen capture semantics with Mouse by [@&#8203;miloush](https://github.com/miloush) in [#&#8203;21363](https://github.com/AvaloniaUI/Avalonia/pull/21363)

**Full Changelog**: <https://github.com/AvaloniaUI/Avalonia/compare/11.3.15...11.3.16>

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNzguMCIsInVwZGF0ZWRJblZlciI6IjQzLjE3OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/122
2026-05-31 10:17:09 +00:00
KeatonTheBot
04574103cf Replace shaderc.net with Silk.NET.Shaderc (#124)
Unpublished branch from gdkchan. Replaces the outdated `shaderc.net` package with `Silk.NET.Shaderc`, which is continually updated with the Silk.NET package suite.

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/124
2026-05-31 10:16:47 +00:00
ranidspace
153d632ee2 Fix the canary CI breaking caused by #110 (#121)
whoops

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/121
2026-05-30 21:02:30 +00:00
Babib3l
824643e143 Fix : Fix controller Stick visualizer perfomance (#119)
This PR aims to fix a bug inside the settings window that caused the Controller Stick visualizer to run at extremely degraded performance when the selected profile had a rainbow LED configuration:
Old behaviour : <video src="/attachments/1915ab74-6e1b-4e2f-9182-820cbed5359b" title="Screen Recording 2026-05-30 105859" controls></video>
New / updated behaviour : <video src="/attachments/dd293402-19e7-439e-8aa6-114d4ee22404" title="Screen Recording 2026-05-30 110338" controls></video>

The issue was that the input settings page automatically subscribed controllers to rainbow LED updates when a saved profile with rainbow LEDs enabled was loaded.
Reopening input settings on a non-default controller profile could immediately start the rainbow LED update loop from the settings UI, which caused the stick visualizer to become sluggish or jittery on affected controllers. The preview handler is now only registered when the rainbow LED option is changed in the settings UI, and it uses a stable handler so duplicate callbacks are not stacked.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/119
2026-05-30 20:33:50 +00:00
ranidspace
7be1708146 AppImage fixes and improvements (#110)
Currently the appimages work fine, however I realized there's some room to improve on following the standards and a bit of cleaning.

AppImages now follow the linux FHS and a `.DirIcon` to follow the Spec closer, along with changing the icon and desktop file names (the resulting AppImage will have the same name, it's only used internally)

Metadata has been added, it helps with applications meant to manage appimages, as well as a future possible auto-updater.

The icon has been replaced temporarily as we wait for the updated one, just taken from the windows .ico. The `.DirIcon` needs to be a PNG, however `app.ryujinx.Ryujinx.png` can be an svg as well for some reason.

The launcher script may not need to exist (gamemode and envvars), but as to not break anything the important things have been kept.

The commits messages of this PR contain information as to why each change was made.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/110
2026-05-30 20:31:49 +00:00
Babib3l
279c4cf3b4 Fix hotkey labels (#118)
Fixes the hotkeys settings page after https://git.ryujinx.app/projects/Ryubing/pulls/13 broke it

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/118
2026-05-30 20:27:44 +00:00
Max
fa72b5a298 Revert "fix/sdl3-gamepad-bugs (#19)" (#116)
This reverts commit 7f0e82fe48.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/116
2026-05-27 14:10:33 +00:00
Babib3l
e02081c09e Wire "Start games in fullscreen" option to use the new fullscreen behaviour (#113)
Final (hopefully) fix for https://github.com/Ryubing/Issues/issues/415

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/113
2026-05-27 12:25:09 +00:00
KeatonTheBot
de70f66a27 macOS: Fix black screen on Metal versions older than 3.1 (#114)
Fixes a black screen on Metal versions older than 3.1 by disabling `VK_EXT_extended_dynamic_state` (and `VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE`). Fix imported from [@sunshineinabox](https://github.com/sunshineinabox)'s unpublished extended dynamic states branch.

Co-authored-by: sunshineinabox <aqemail@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/114
2026-05-27 12:24:41 +00:00
Xam
6c1692ed60 HLE: Applets: Software keyboard: update cursor position on SetInputText (#25)
fixes cursor being at 0 position even when text is present, especially annoying for handheld devices with kde virtual keyboard, which is pretty bad, with no way to move the cursor or delete text backward

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/25
2026-05-27 12:22:05 +00:00
Babib3l
fb7c1fde11 Input: Refactor Keyboard Handling To Use Physical Keys (#13)
This PR refactors keyboard handling to use physical key mappings for all gameplay input, ensuring controls remain consistent across different OS keyboard layouts.
I'd like to give out an ENORMOUS thank you to @Neo for his very generous help on getting MacOS caps lock behaviour working, but as well for taking the time for extensive testing, planning and discussions, and finally for writing this PR message :) Keep being awsome pal 👊

### New Features :
* **New**: Gameplay input now uses physical key positions instead of OS layouts, ensuring the same physical key triggers the same action across keyboard layouts.
    * Key rebinding stores physical keys and config compatibility is preserved, with physical keys now the primary gameplay‑binding format.
    * Physical‑key model is now consistent across platforms, including updated SDL/headless behavior.
* **Added**: New Input setting "Reset keybinds to default", with a new confirmation dialog appears when changes are being overwritten.
* **Fractured**: Keyboard‑related locales to the newly created `KeyboardLayout.json`.
    * New input device settings/actions use clearer labels and tooltips.
    * UI Key Labels (such as Left Shift and Right Shift) are more accurate and standardized, with clearer symbols, consistent naming, dynamic learning of printable labels from real key events, and persistence across restarts.
### Improvements :
* **Reduced**: Incorrect key labels by using observed host symbols instead of language assumptions.
* **Reduced**: Stuck/stale keys by using binary pressed‑key tracking, fixing rebinding/gameplay paths, better held‑key recovery after focus changes, and clearing keyboard state when Ryujinx/settings windows lose focus.
* **Improved**: Device handling → refreshing no longer clears the selector, disconnect fallback is consistent, reconnect restores controllers automatically, and the UI avoids invalid/empty device states.
* **Improved**: Async input‑assignment callbacks are now guarded when switching views/devices, preventing stale callbacks from hitting detached views.
* **Adjusted**: Input visualiser to be more robust when switching sources or handling controller disconnect/reconnect, without needing to reopen settings.
* **Improved**: Modification (changes to input controls) tracking
    * Rebinding to the same value, reverting to original config, restoring defaults without differences, or reloading equivalent profiles no longer leaves Player marked as modified.
* **Reduced**: Keyboard LED noise in logs and added optional UI keyboard‑state/rebinding diagnostics.
### Fixes :
* **Special Keys**:
    * AltGr and other special keys behave correctly, including proper Ctrl+Alt → AltRight handling and more consistent normalization of special/synthetic keys.
    * Caps Lock is now reliably bindable on all platforms (Windows/Linux register every press; macOS every other).
* **Fixed**: Certain cases where keyboard input broke after pointer interactions
### Current Limitations

These are planned on being fixed/improved upon in future PRs:
* Hotkeys still use semantic (Key) mappings.
* Software keyboard / text input still uses the semantic path
* Printable key labels may fall back to defaults until observed from host input.
* Full semantic/physical split currently implemented only in the Avalonia driver.

Co-authored-by: _Neo_ <ursamajorjanus2819@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/13
2026-05-26 17:54:55 +00:00
LotP
18226decf1 check for HandleDesc (#112)
HandleDesc can be null, make sure to check for that.

Co-authored-by: LotP1 <68976644+LotP1@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/112
2026-05-25 13:43:35 +00:00
Babib3l
b2310823c9 Windows Fullscreen Fixes (#87)
This PR fixes two bugs introduced in #80 :
- Exiting fullscreen could exit the Ryujinx window  (https://github.com/Ryubing/Issues/issues/415)
- Toggling fullscreen on would move the window to the primary monitor

And two other bugs related to input view :
- Exiting fullscreen when window was previously in a non maximized state now conserves the window coordinates and size properly (https://github.com/Ryubing/Issues/issues/425)
- Opening the ryujinx app no makes the app grow slightly larger vertically on each launch (+31px on my 1080p monitor) (https://github.com/Ryubing/Issues/issues/425)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/87
2026-05-25 12:09:00 +00:00
Babib3l
b62c58c2fe River : HLE: Make process identity explicit for service metadata resolution (#100)
This PR is the first in a batch of structural changes to Ryujinx.

**Changes**

- Added `ProcessIdentity` and `ProcessKind` to describe loaded programs by:
  - PID, program ID, application ID, program index, display version, process kind
- Stored identity metadata on `ProcessResult`.
- Added PID-based process lookup helpers to `ProcessLoader`.
- Updated HLE services to resolve application metadata through the caller PID instead of `Processes.ActiveApplication`.
- Added PTC/JIT disk cache initialization logging with PID, title ID, display version, selector, and enabled state.
- Added `ClientProcessId` property to ServiceCtx (/src/Ryujinx.HLE/HOS/ServiceCtx.cs) that uses the handle descriptor PId when available, falling back to `Process.Pid`.
- Updated 15 HLE service files to use `context.ClientProcessId` instead of `context.Process.Pid` for client process access, ensuring services correctly identify the calling process even when invoked via IPC with handle descriptors.

These changes make service metadata resolution more explicit and prepare the emulator for other structural changes later on.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/100
2026-05-25 12:08:31 +00:00
109 changed files with 6385 additions and 4076 deletions

View File

@@ -68,7 +68,7 @@ jobs:
sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
sed -r --in-place '/^Name=Ryujinx$/s/Name=Ryujinx/Name=Ryujinx-Canary/' distribution/linux/Ryujinx.desktop
sed -r --in-place '/^Name=Ryujinx$/s/Name=Ryujinx/Name=Ryujinx-Canary/' distribution/linux/app.ryujinx.Ryujinx.desktop
shell: bash
- name: Create output dir

3
.gitignore vendored
View File

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

View File

@@ -3,12 +3,12 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.15" />
<PackageVersion Include="Avalonia" Version="11.3.17" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.15" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.15" />
<PackageVersion Include="SharpCompress" Version="0.48.1" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.17" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.17" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.17" />
<PackageVersion Include="SharpCompress" Version="0.49.1" />
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.9.5" />
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.9.5" />
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
@@ -27,7 +27,7 @@
<PackageVersion Include="Humanizer" Version="2.14.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.18.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.19.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
@@ -43,7 +43,7 @@
<PackageVersion Include="Open.NAT.Core" Version="2.1.0.5" />
<!-- Ryujinx.Audio.OpenAL.Dependencies is from the original project, last updated 12/30/20 -->
<!--<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />-->
<PackageVersion Include="Ryujinx.Audio.OpenAL" Version="1.25.1" />
<PackageVersion Include="Ryujinx.Audio.OpenAL" Version="1.25.2" />
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.4-build6" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.MoltenVK" Version="1.4.2-ryujinx.3" />
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" />
@@ -51,9 +51,9 @@
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="2.0.6" />
<PackageVersion Include="Gommon" Version="2.8.1.2" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.14.1" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.15.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Silk.NET.Shaderc" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
@@ -63,6 +63,6 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" />
<PackageVersion Include="SPB" Version="0.0.4-build32" />
<PackageVersion Include="System.IO.Hashing" Version="9.0.15" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.1.3" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.1.0" />
</ItemGroup>
</Project>

View File

@@ -40,8 +40,14 @@
## Usage
To run this emulator, your PC must be equipped with at least 8GiB of RAM;
failing to meet this requirement may result in a poor gameplay experience or unexpected crashes.
To run this emulator, your PC must be equipped with at least:
- 8GiB of RAM
- 6 cores
- A GPU released within the last 10 years.
- OpenGL 4.6 | Vulkan 1.4
- Windows 10 version 20H1 | macOS Big Sur (Apple Silicon)
Failing to meet these requirements may result in a poor gameplay experience or unexpected crashes.
## Latest build

View File

@@ -0,0 +1,104 @@
{
"Locales": [
{
"ID": "UpdatesAddedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Updates Added: {0}",
"es_ES": "Actualizaciones Añadidas: {0}",
"fr_FR": "Mises à Jour Ajoutées : {0}",
"he_IL": "",
"it_IT": "Aggiornamenti aggiunti: {0}",
"ja_JP": "",
"ko_KR": "추가된 업데이트: {0}",
"no_NO": "Oppdateringer lagt til: {0}",
"pl_PL": "",
"pt_BR": "Atualizações adicionadas: {0}",
"ru_RU": "Добавлено обновлений: {0}",
"sv_SE": "Tillagda uppdateringar: {0}",
"th_TH": "การอัปเดตที่เพิ่มเข้ามา: {0}",
"tr_TR": "",
"uk_UA": "Додані оновлення: {0}",
"zh_CN": "已添加更新:{0}",
"zh_TW": "已新增更新:{0}"
}
},
{
"ID": "UpdatesRemovedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Updates Removed: {0}",
"es_ES": "Actualizaciones Eliminadas: {0}",
"fr_FR": "Mises à Jour Supprimées : {0}",
"he_IL": "",
"it_IT": "Aggiornamenti rimossi: {0}",
"ja_JP": "",
"ko_KR": "제거된 업데이트: {0}",
"no_NO": "Fjernede oppdateringer: {0}",
"pl_PL": "",
"pt_BR": "Atualizações removidas: {0}",
"ru_RU": "Удалено обновлений: {0}",
"sv_SE": "Borttagna uppdateringar: {0}",
"th_TH": "การอัปเดตที่ถูกลบ: {0}",
"tr_TR": "",
"uk_UA": "Видалені оновлення: {0}",
"zh_CN": "已移除更新:{0}",
"zh_TW": "已移除更新:{0}"
}
},
{
"ID": "DLCAddedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "DLC Added: {0}",
"es_ES": "DLC Añadidos: {0}",
"fr_FR": "DLC Ajoutés : {0}",
"he_IL": "",
"it_IT": "DLC aggiunti: {0}",
"ja_JP": "",
"ko_KR": "추가된 DLC: {0}",
"no_NO": "DLC lagt til: {0}",
"pl_PL": "",
"pt_BR": "DLC adicionados: {0}",
"ru_RU": "Добавлено DLC: {0}",
"sv_SE": "Tillagda DLC: {0}",
"th_TH": "DLC ที่เพิ่มเข้ามา: {0}",
"tr_TR": "",
"uk_UA": "Додані DLC: {0}",
"zh_CN": "已添加 DLC{0}",
"zh_TW": "已新增 DLC{0}"
}
},
{
"ID": "DLCRemovedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "DLC Removed: {0}",
"es_ES": "DLC Eliminados: {0}",
"fr_FR": "DLC Supprimés : {0}",
"he_IL": "",
"it_IT": "DLC rimossi: {0}",
"ja_JP": "",
"ko_KR": "제거된 DLC: {0}",
"no_NO": "DLC fjernet: {0}",
"pl_PL": "",
"pt_BR": "DLC removidos: {0}",
"ru_RU": "Удалено DLC: {0}",
"sv_SE": "DLC borttaget: {0}",
"th_TH": "ลบ DLC: {0}",
"tr_TR": "",
"uk_UA": "Видалено DLC: {0}",
"zh_CN": "已移除 DLC{0}",
"zh_TW": "已移除 DLC{0}"
}
}
]
}

View File

@@ -0,0 +1,79 @@
{
"Locales": [
{
"ID": "LoadApplicationFromFileFilePickerTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Select a Switch application file to load",
"es_ES": "Selecciona un archivo de aplicación de Switch para cargar",
"fr_FR": "Sélectionnez un fichier dapplication Switch à charger",
"he_IL": "",
"it_IT": "Seleziona un file applicazione Switch da caricare",
"ja_JP": "",
"ko_KR": "로드할 Switch 애플리케이션 파일을 선택하세요",
"no_NO": "",
"pl_PL": "",
"pt_BR": "Selecione um arquivo de aplicativo Switch para carregar",
"ru_RU": "Выберите файл приложения Switch для загрузки",
"sv_SE": "Välj en Switch-applikationsfil att läsa in",
"th_TH": "",
"tr_TR": "",
"uk_UA": "Виберіть файл застосунку Switch для завантаження",
"zh_CN": "请选择要加载的 Switch 应用程序文件",
"zh_TW": "請選擇要載入的 Switch 應用程式檔案"
}
},
{
"ID": "LoadUnpackedApplicationFromFolderFilePickerTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Select a folder containing an unpacked Switch application to load",
"es_ES": "Selecciona una carpeta que contenga una aplicación de Switch descomprimida para cargar",
"fr_FR": "Sélectionnez un dossier contenant une application Switch décompressée à charger",
"he_IL": "",
"it_IT": "Seleziona una cartella contenente unapplicazione Switch non compressa da caricare",
"ja_JP": "",
"ko_KR": "압축 해제된 Switch 애플리케이션이 포함된 폴더를 선택하여 로드하세요",
"no_NO": "",
"pl_PL": "",
"pt_BR": "Selecione uma pasta que contenha um aplicativo Switch descompactado para carregar",
"ru_RU": "Выберите папку, содержащую распакованное приложение Switch, для загрузки",
"sv_SE": "Välj en mapp som innehåller en uppackad Switch-applikation att läsa in",
"th_TH": "",
"tr_TR": "",
"uk_UA": "Виберіть папку, що містить розпакований застосунок Switch, для завантаження",
"zh_CN": "请选择包含未打包 Switch 应用程序的文件夹以加载",
"zh_TW": "請選擇包含未解壓縮 Switch 應用程式的資料夾以載入"
}
},
{
"ID": "LoadUpdatesAndDLCFromFolderFilePickerTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Select one or more folders to bulk load updates and DLC from",
"es_ES": "Selecciona una o más carpetas para cargar de forma masiva actualizaciones y DLC",
"fr_FR": "Sélectionnez un ou plusieurs dossiers pour charger en masse des mises à jour et des DLC",
"he_IL": "",
"it_IT": "Seleziona una o più cartelle da cui caricare in blocco aggiornamenti e DLC",
"ja_JP": "",
"ko_KR": "업데이트 및 DLC를 대량으로 로드할 폴더를 하나 이상 선택하세요",
"no_NO": "",
"pl_PL": "",
"pt_BR": "Selecione uma ou mais pastas para carregar em massa atualizações e DLC",
"ru_RU": "Выберите одну или несколько папок для массовой загрузки обновлений и DLC",
"sv_SE": "Välj en eller flera mappar för att massinläsa uppdateringar och DLC",
"th_TH": "",
"tr_TR": "",
"uk_UA": "Виберіть одну або кілька папок для масового завантаження оновлень і DLC",
"zh_CN": "请选择一个或多个文件夹以批量加载更新和 DLC",
"zh_TW": "請選擇一個或多個資料夾以批次載入更新與 DLC"
}
}
]
}

View File

@@ -0,0 +1,104 @@
{
"Locales": [
{
"ID": "AssociationSuccessMessage",
"Translations": {
"ar_SA": "تم ربط أنواع الملفات بنجاح.",
"de_DE": "Dateitypen erfolgreich zugeordnet.",
"el_GR": "Οι τύποι αρχείων συσχετίστηκαν με επιτυχία.",
"en_US": "Successfully associated file types.",
"es_ES": "¡Tipos de archivo asociados con éxito.",
"fr_FR": "Types de fichiers associés avec succès.",
"he_IL": "שיוך סוגי הקבצים בוצע בהצלחה.",
"it_IT": "Tipi di file associati con successo.",
"ja_JP": "ファイルの種類の関連付けに成功しました。",
"ko_KR": "파일 형식 연결에 성공했습니다.",
"no_NO": "Filtyper ble tilknyttet.",
"pl_PL": "Pomyślnie powiązano typy plików.",
"pt_BR": "Tipos de arquivo associados com sucesso.",
"ru_RU": "Типы файлов успешно связаны.",
"sv_SE": "Filtyper har kopplats.",
"th_TH": "เชื่อมโยงประเภทไฟล์สำเร็จแล้ว",
"tr_TR": "Dosya türleri başarıyla ilişkilendirildi.",
"uk_UA": "Типи файлів успішно пов’язані.",
"zh_CN": "文件类型关联成功。",
"zh_TW": "檔案類型關聯成功。"
}
},
{
"ID": "RemoveAssociationSuccessMessage",
"Translations": {
"ar_SA": "تمت إزالة ارتباطات أنواع الملفات بنجاح.",
"de_DE": "Dateitypzuordnungen erfolgreich entfernt.",
"el_GR": "Οι συσχετίσεις τύπων αρχείων αφαιρέθηκαν με επιτυχία.",
"en_US": "Successfully removed file type associations.",
"es_ES": "Asociaciones de tipos de archivo eliminadas con éxito.",
"fr_FR": "Associations de types de fichiers supprimées avec succès.",
"he_IL": "שיוכי סוגי הקבצים הוסרו בהצלחה.",
"it_IT": "Associazioni dei tipi di file rimosse con successo.",
"ja_JP": "ファイルの種類の関連付けの削除に成功しました。",
"ko_KR": "파일 형식 연결이 성공적으로 제거되었습니다.",
"no_NO": "Filtype-tilknytninger ble fjernet.",
"pl_PL": "Pomyślnie usunięto skojarzenia typów plików.",
"pt_BR": "Associações de tipos de arquivo removidas com sucesso.",
"ru_RU": "Связи типов файлов успешно удалены.",
"sv_SE": "Filtypsassociationer har tagits bort.",
"th_TH": "ลบการเชื่อมโยงประเภทไฟล์สำเร็จแล้ว",
"tr_TR": "Dosya türü ilişkilendirmeleri başarıyla kaldırıldı.",
"uk_UA": "Прив’язки типів файлів успішно видалено.",
"zh_CN": "文件类型关联已成功移除。",
"zh_TW": "檔案類型關聯已成功移除。"
}
},
{
"ID": "AssociationFailedMessage",
"Translations": {
"ar_SA": "فشل ربط أنواع الملفات.",
"de_DE": "Fehler beim Zuordnen der Dateitypen.",
"el_GR": "Απέτυχε η συσχέτιση των τύπων αρχείων.",
"en_US": "Failed to associate file types.",
"es_ES": "No se pudieron asociar los tipos de archivo.",
"fr_FR": "Échec de lassociation des types de fichiers.",
"he_IL": "שיוך סוגי הקבצים נכשל.",
"it_IT": "Associazione dei tipi di file non riuscita.",
"ja_JP": "ファイルの種類の関連付けに失敗しました。",
"ko_KR": "파일 형식 연결에 실패했습니다.",
"no_NO": "Klarte ikke å tilknytte filtyper.",
"pl_PL": "Nie udało się powiązać typów plików.",
"pt_BR": "Falha ao associar tipos de arquivo.",
"ru_RU": "Не удалось связать типы файлов.",
"sv_SE": "Det gick inte att koppla filtyper.",
"th_TH": "เชื่อมโยงประเภทไฟล์ไม่สำเร็จ",
"tr_TR": "Dosya türleri ilişkilendirilemedi.",
"uk_UA": "Не вдалося пов’язати типи файлів.",
"zh_CN": "文件类型关联失败。",
"zh_TW": "檔案類型關聯失敗。"
}
},
{
"ID": "RemoveAssociationFailedMessage",
"Translations": {
"ar_SA": "فشلت إزالة ارتباطات أنواع الملفات.",
"de_DE": "Das Entfernen der Dateitypzuordnungen ist fehlgeschlagen.",
"el_GR": "Η αφαίρεση των συσχετίσεων τύπων αρχείων απέτυχε.",
"en_US": "Failed to remove file type associations.",
"es_ES": "No se pudieron eliminar las asociaciones de tipos de archivo.",
"fr_FR": "Échec de la suppression des associations de types de fichiers.",
"he_IL": "הסרת שיוכי סוגי הקבצים נכשלה.",
"it_IT": "Rimozione delle associazioni dei tipi di file non riuscita.",
"ja_JP": "ファイルの種類の関連付けの削除に失敗しました。",
"ko_KR": "파일 형식 연결 제거에 실패했습니다.",
"no_NO": "Fjerning av filtype-tilknytninger mislyktes.",
"pl_PL": "Nie udało się usunąć skojarzeń typów plików.",
"pt_BR": "Falha ao remover associações de tipos de arquivo.",
"ru_RU": "Не удалось удалить связи типов файлов.",
"sv_SE": "Det gick inte att ta bort filtypsassociationer.",
"th_TH": "ไม่สามารถลบการเชื่อมโยงประเภทไฟล์ได้",
"tr_TR": "Dosya türü ilişkilendirmeleri kaldırılamadı.",
"uk_UA": "Не вдалося видалити прив’язки типів файлів.",
"zh_CN": "文件类型关联移除失败。",
"zh_TW": "無法移除檔案類型關聯。"
}
}
]
}

View File

@@ -1,5 +1,55 @@
{
"Locales": [
{
"ID": "NoApplicationFoundInFile",
"Translations": {
"ar_SA": "",
"de_DE": "Keine Anwendungen in ausgewählter Datei gefunden.",
"el_GR": "",
"en_US": "No applications found in selected file.",
"es_ES": "No se encontraron aplicaciones en el archivo seleccionado.",
"fr_FR": "Aucune application trouvée dans le fichier sélectionné.",
"he_IL": "",
"it_IT": "Nessuna applicazione trovata nel file selezionato.",
"ja_JP": "",
"ko_KR": "선택한 파일에서 앱을 찾을 수 없습니다.",
"no_NO": "Ingen apper ble funnet i valgt fil.",
"pl_PL": "",
"pt_BR": "Nenhum aplicativo encontrado no arquivo selecionado.",
"ru_RU": "Приложений в выбранном файле не найдены",
"sv_SE": "Inga applikationer hittades i vald fil.",
"th_TH": "ไม่พบแอปพลิเคชั่นจากไฟล์ที่เลือก",
"tr_TR": "",
"uk_UA": "У вибраному файлі не знайдено жодних додатків.",
"zh_CN": "未发现应用",
"zh_TW": "未能從已選擇的檔案中找到應用程式。"
}
},
{
"ID": "NoUnpackedApplicationFoundInFolder",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select an unpacked application folder with a valid ExeFS or NSO/NRO.",
"es_ES": "",
"fr_FR": "Veuillez sélectionner un répertoire dapplication décompressée contenant un ExeFS valide ou NSO/NRO.",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "Пожалуйста, выберите папку распакованного приложения с корректным ExeFS или NSO/NRO",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "NoKeysFound",
"Translations": {

File diff suppressed because it is too large Load Diff

View File

@@ -1,78 +1,278 @@
{
"Locales": [
{
"ID": "ManageFileTypes",
"ID": "FileLabel",
"Translations": {
"ar_SA": "إدارة أنواع الملفات",
"de_DE": "Dateitypen verwalten",
"el_GR": "Διαχείριση τύπων αρχείων",
"en_US": "Manage File Types",
"es_ES": "Administrar Tipos de Archivo",
"fr_FR": "Gérer les Types de Fichiers",
"he_IL": "ניהול סוגי קבצים",
"it_IT": "Gestisci i tipi di file",
"ja_JP": "ファイル形式を管理",
"ko_KR": "파일 형식 관리",
"no_NO": "Behandle filtyper",
"pl_PL": "Zarządzaj rodzajami plików",
"pt_BR": "Gerenciar Tipos de Arquivos",
"ru_RU": "Управление типами файлов",
"sv_SE": "Hantera filtyper",
"th_TH": "จัดการประเภทไฟล์",
"tr_TR": "Dosya uzantılarını yönet",
"uk_UA": "Керувати типами файлів",
"zh_CN": "管理文件扩展名",
"zh_TW": "管理檔案類型"
"ar_SA": "_ملف",
"de_DE": "_Datei",
"el_GR": "_Αρχείο",
"en_US": "_File",
"es_ES": "_Archivo",
"fr_FR": "_Fichier",
"he_IL": "ובץ",
"it_IT": "_Archivio",
"ja_JP": "ファイル(_F)",
"ko_KR": "파일(_F)",
"no_NO": "_Fil",
"pl_PL": "_Plik",
"pt_BR": "_Arquivo",
"ru_RU": "айл",
"sv_SE": "_Arkiv",
"th_TH": "ไฟล์",
"tr_TR": "_Dosya",
"uk_UA": "айл",
"zh_CN": "文件(_F)",
"zh_TW": "檔案(_F)"
}
},
{
"ID": "InstallFileTypes",
"ID": "LoadApplicationFromFileButton",
"Translations": {
"ar_SA": "تثبيت أنواع الملفات",
"de_DE": "Dateitypen installieren",
"el_GR": "Εγκαταστήσετε τύπους αρχείων.",
"en_US": "Install File Types",
"es_ES": "Instalar Tipos de Archivo",
"fr_FR": "Installer des Types de Fichiers",
"he_IL": "סוגי קבצי התקנה",
"it_IT": "Installa i tipi di file",
"ja_JP": "ファイル形式をインストール",
"ko_KR": "파일 형식 설치",
"no_NO": "Installer filtyper",
"pl_PL": "Typy plików instalacyjnych",
"pt_BR": "Instalar tipos de arquivos",
"ru_RU": "Установить типы файлов",
"sv_SE": "Installera filtyper",
"th_TH": "ติดตั้งประเภทไฟล์",
"tr_TR": "Dosya uzantılarını yükle",
"uk_UA": "Встановити типи файлів",
"zh_CN": "关联文件扩展名",
"zh_TW": "安裝檔案類型"
"ar_SA": "_تحميل التطبيق...",
"de_DE": "_Anwendung laden...",
"el_GR": "_Φόρτωση εφαρμογής...",
"en_US": "_Load Application...",
"es_ES": "_Cargar Aplicación...",
"fr_FR": "_Charger lApplication...",
"he_IL": "_טען יישום...",
"it_IT": "_Carica applicazione...",
"ja_JP": "アプリケーションをロード(_L)...",
"ko_KR": "앱 불러오기(_L)...",
"no_NO": "_Last inn program...",
"pl_PL": "_Załaduj aplikację...",
"pt_BR": "_Carregar aplicativo...",
"ru_RU": "_Загрузить приложение...",
"sv_SE": "_Läs in applikation...",
"th_TH": "_โหลดแอปพลิเคชัน...",
"tr_TR": "_Uygulamayı yükle...",
"uk_UA": "_Завантажити застосунок...",
"zh_CN": "加载应用程序(_L)...",
"zh_TW": "載入應用程式(_L)..."
}
},
{
"ID": "UninstallFileTypes",
"ID": "LoadUnpackedGameFromFolderButton",
"Translations": {
"ar_SA": "إزالة أنواع الملفات",
"de_DE": "Dateitypen deinstallieren",
"el_GR": "Απεγκαταστήσετε τύπους αρχείων",
"en_US": "Uninstall File Types",
"es_ES": "Desinstalar Tipos de Archivo",
"fr_FR": "Désinstaller des Types de Fichiers",
"he_IL": "סוגי קבצי הסרה",
"it_IT": "Disinstalla i tipi di file",
"ja_JP": "ファイル形式をアンインストール",
"ko_KR": "파일 형식 제거",
"no_NO": "Avinstaller filtyper",
"pl_PL": "Typy plików dezinstalacyjnych",
"pt_BR": "Desinstalar tipos de arquivos",
"ru_RU": "Удалить типы файлов",
"sv_SE": "Avinstallera filtyper",
"th_TH": "ถอนการติดตั้งประเภทไฟล์",
"tr_TR": "Dosya uzantılarını kaldır",
"uk_UA": "Видалити типи файлів",
"zh_CN": "取消关联扩展名",
"zh_TW": "移除檔案類型"
"ar_SA": "تحميل لُعْبَة غير محزومة...",
"de_DE": "_Entpacktes Spiel laden...",
"el_GR": "_Φόρτωση Απακετάριστου Παιχνιδιού...",
"en_US": "Load _Unpacked Game...",
"es_ES": "Cargar Juego _Desempaquetado...",
"fr_FR": "Charger un Jeu Décompressé...",
"he_IL": "טען משחק _שאינו ארוז...",
"it_IT": "Carica gioco _estratto...",
"ja_JP": "_展開されたゲームをロード...",
"ko_KR": "압축 푼 게임 불러오기(_U)...",
"no_NO": "Last inn _upakket spill...",
"pl_PL": "Załaduj _rozpakowaną grę...",
"pt_BR": "Abrir Jogo _Extraído...",
"ru_RU": "Загрузить _распакованную игру...",
"sv_SE": "Läs in _uppackat spel...",
"th_TH": "โหลดเกมที่แตกไฟล์แล้ว...",
"tr_TR": "_Sıkıştırılmamış Oyun Yükle...",
"uk_UA": "Завантажити _розпаковану гру...",
"zh_CN": "加载解包后的游戏(_U)...",
"zh_TW": "載入未封裝的遊戲(_U)..."
}
},
{
"ID": "LoadTitleUpdatesAndDLCFromFolderButton",
"Translations": {
"ar_SA": "",
"de_DE": "Updates/DLC laden...",
"el_GR": "",
"en_US": "Load Updates/DLC...",
"es_ES": "Cargar Actualizaciones/DLC...",
"fr_FR": "Charger les Mises à Jour/DLC...",
"he_IL": "",
"it_IT": "Carica aggiornamenti/DLC...",
"ja_JP": "",
"ko_KR": "업데이트/DLC 불러오기...",
"no_NO": "Last inn oppdateringer/DLC...",
"pl_PL": "",
"pt_BR": "Carregar atualizações/DLC...",
"ru_RU": "Загрузить обновления/DLC...",
"sv_SE": "Läs in uppdateringar/DLC...",
"th_TH": "โหลดอัปเดต/DLC...",
"tr_TR": "",
"uk_UA": "Завантажити оновлення/DLC...",
"zh_CN": "加载更新/DLC...",
"zh_TW": "載入更新/DLC..."
}
},
{
"ID": "OpenRyuijnxFolderButton",
"Translations": {
"ar_SA": "‫فتح مجلد Ryujinx",
"de_DE": "Ryujinx-Ordner öffnen",
"el_GR": "Άνοιγμα Φακέλου Ryujinx",
"en_US": "Open Ryujinx Folder",
"es_ES": "Abrir Carpeta de Ryujinx",
"fr_FR": "Ouvrir le Dossier Ryujinx",
"he_IL": "פתח את תיקיית ריוג'ינקס",
"it_IT": "Apri la cartella di Ryujinx",
"ja_JP": "Ryujinx フォルダを開く",
"ko_KR": "Ryujinx 폴더 열기",
"no_NO": "Åpne Ryujinx mappe",
"pl_PL": "Otwórz folder Ryujinx",
"pt_BR": "Abrir Pasta do Ryujinx",
"ru_RU": "Открыть папку Ryujinx",
"sv_SE": "Öppna Ryujinx-mapp",
"th_TH": "เปิดโฟลเดอร์ Ryujinx",
"tr_TR": "Ryujinx Klasörünü aç",
"uk_UA": "Відкрити теку Ryujinx",
"zh_CN": "打开 Ryujinx 系统目录",
"zh_TW": "開啟 Ryujinx 資料夾"
}
},
{
"ID": "OpenLogsFolderButton",
"Translations": {
"ar_SA": "فتح مجلد السجلات",
"de_DE": "Logs-Ordner öffnen",
"el_GR": "Άνοιγμα Φακέλου Καταγραφής",
"en_US": "Open Logs Folder",
"es_ES": "Abrir Carpeta de Registros",
"fr_FR": "Ouvrir le Dossier des Journaux",
"he_IL": "פתח את תיקיית קבצי הלוג",
"it_IT": "Apri la cartella dei log",
"ja_JP": "ログフォルダを開く",
"ko_KR": "로그 폴더 열기",
"no_NO": "Åpne Logg mappen",
"pl_PL": "Otwórz folder plików dziennika zdarzeń",
"pt_BR": "Abrir Pasta de _Logs",
"ru_RU": "Открыть папку журналов",
"sv_SE": "Öppna loggmapp",
"th_TH": "เปิดโฟลเดอร์ Logs",
"tr_TR": "Logs Klasörünü aç",
"uk_UA": "Відкрити теку журналів змін",
"zh_CN": "打开日志目录",
"zh_TW": "開啟日誌資料夾"
}
},
{
"ID": "OpenScreenshotsFolderButton",
"Translations": {
"ar_SA": "",
"de_DE": "Screenshots-Ordner öffnen",
"el_GR": "",
"en_US": "Open Screenshots Folder",
"es_ES": "Abrir Carpeta de Capturas de Pantalla",
"fr_FR": "Ouvrir le Dossier des Captures dÉcran",
"he_IL": "",
"it_IT": "Apri la cartella degli screenshots",
"ja_JP": "",
"ko_KR": "스크린샷 폴더 열기",
"no_NO": "Åpne Skjermbilde Mappen",
"pl_PL": "",
"pt_BR": "Abrir Pasta de Capturas de Tela",
"ru_RU": "Открыть папку снимков экрана",
"sv_SE": "Öppna skärmbildsmappen",
"th_TH": "เปิดโฟลเดอร์ที่เก็บภาพหน้าจอ",
"tr_TR": "",
"uk_UA": "Відкрити теку скріншотів",
"zh_CN": "打开截图文件夹",
"zh_TW": "開啟螢幕擷取畫面資料夾"
}
},
{
"ID": "AssociateFileTypesButton",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Associate File Types",
"es_ES": "",
"fr_FR": "Associer Les Types de Fichiers",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "Связать файлы с приложением",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "RemoveFileTypeAssociationsButton",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Remove File Type Associations",
"es_ES": "",
"fr_FR": "Dissocier Les Types de Fichiers",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "Удалить ассоциации файлов",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "FileTypeAssociationsToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Associates supported file types (NSP, XCI, NCA, NRO, NSO) with Ryujinx so they open automatically when double-clicked in your file manager. Removing file type associations stops this behavior.",
"es_ES": "",
"fr_FR": "Associe les types de fichiers pris en charge (NSP, XCI, NCA, NRO, NSO) à Ryujinx afin quils souvrent automatiquement par double-clic dans votre gestionnaire de fichiers. Les dissocier désactive ce comportement",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "Связывает поддерживаемые типы файлов (NSP, XCI, NCA, NRO, NSO) с Ryujinx, чтобы они автоматически открывались при двойном щелчке в файловом менеджере. Удаление ассоциаций файлов отключает это поведение.",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "ExitButton",
"Translations": {
"ar_SA": "_خروج",
"de_DE": "_Beenden",
"el_GR": "_Έξοδος",
"en_US": "_Exit",
"es_ES": "_Salir",
"fr_FR": "_Quitter",
"he_IL": "_יציאה",
"it_IT": "_Esci",
"ja_JP": "終了(_E)",
"ko_KR": "종료(_E)",
"no_NO": "_Avslutt",
"pl_PL": "_Wyjdź",
"pt_BR": "_Sair",
"ru_RU": "_Выйти",
"sv_SE": "_Avsluta",
"th_TH": "_ออก",
"tr_TR": "_Çıkış",
"uk_UA": "_Вихід",
"zh_CN": "退出(_E)",
"zh_TW": "結束(_E)"
}
}
]

View File

@@ -0,0 +1,104 @@
{
"Locales": [
{
"ID": "OptionsLabel",
"Translations": {
"ar_SA": "_خيارات",
"de_DE": "_Optionen",
"el_GR": "_Επιλογές",
"en_US": "_Options",
"es_ES": "_Opciones",
"fr_FR": null,
"he_IL": "_אפשרויות",
"it_IT": "_Opzioni",
"ja_JP": "オプション(_O)",
"ko_KR": "옵션(_O)",
"no_NO": "_Alternativer",
"pl_PL": "_Opcje",
"pt_BR": "_Opções",
"ru_RU": "_Настройки",
"sv_SE": "_Inställningar",
"th_TH": "_ตัวเลือก",
"tr_TR": "_Seçenekler",
"uk_UA": "_Параметри",
"zh_CN": "选项(_O)",
"zh_TW": "選項(_O)"
}
},
{
"ID": "LanguageButton",
"Translations": {
"ar_SA": "اللغة",
"de_DE": "Sprache",
"el_GR": "Γλώσσα",
"en_US": "Language",
"es_ES": "Idioma",
"fr_FR": "Langue",
"he_IL": "שפה",
"it_IT": "Lingua",
"ja_JP": "言語",
"ko_KR": "언어",
"no_NO": "Språk",
"pl_PL": "Język",
"pt_BR": "Idioma",
"ru_RU": "Язык",
"sv_SE": "Språk",
"th_TH": "ภาษา",
"tr_TR": "Dil",
"uk_UA": "Мова",
"zh_CN": "语言",
"zh_TW": "語言"
}
},
{
"ID": "UserProfilesButton",
"Translations": {
"ar_SA": "_ملفات المستخدمين",
"de_DE": "_Benutzerprofile",
"el_GR": "_Προφίλ Χρηστών",
"en_US": "_User Profiles",
"es_ES": "_Perfiles de Usuario",
"fr_FR": "_Profils d'Utilisateurs",
"he_IL": "_פרופילי משתמש",
"it_IT": "_Profili utente",
"ja_JP": "ユーザプロファイル(_M)",
"ko_KR": "사용자 프로필(_M)",
"no_NO": "_Brukerprofiler",
"pl_PL": "_Profile użytkowników",
"pt_BR": "_Perfis de usuário",
"ru_RU": "_Профили пользователей",
"sv_SE": "_Användarprofiler",
"th_TH": "_โปรไฟล์ผู้ใช้งาน",
"tr_TR": "_Kullanıcı Profilleri",
"uk_UA": "_Профілі користувачів",
"zh_CN": "用户配置文件(_M)",
"zh_TW": "使用者設定檔(_M)"
}
},
{
"ID": "SettingsButton",
"Translations": {
"ar_SA": "_الإعدادات",
"de_DE": "_Einstellungen",
"el_GR": "_Ρυθμίσεις",
"en_US": "_Settings",
"es_ES": "_Configuración",
"fr_FR": "_Paramètres",
"he_IL": "_הגדרות",
"it_IT": "_Impostazioni",
"ja_JP": "設定(_S)",
"ko_KR": "설정(_S)",
"no_NO": "_Innstillinger",
"pl_PL": "_Ustawienia",
"pt_BR": "_Configurações",
"ru_RU": "_Параметры",
"sv_SE": "_Inställningar",
"th_TH": "_ตั้งค่า",
"tr_TR": "_Seçenekler",
"uk_UA": "_Налаштування",
"zh_CN": "设置(_S)",
"zh_TW": "設定(_S)"
}
}
]
}

View File

@@ -0,0 +1,54 @@
{
"Locales": [
{
"ID": "ToggleFullscreenButton",
"Translations": {
"ar_SA": "التبديل إلى وضع ملء الشاشة",
"de_DE": "Vollbild",
"el_GR": "Λειτουργία Πλήρους Οθόνης",
"en_US": "Toggle Fullscreen",
"es_ES": "Cambiar a Pantalla Completa.",
"fr_FR": "Basculer en Plein Écran",
"he_IL": "שנה מצב- מסך מלא",
"it_IT": "Schermo intero",
"ja_JP": "全画面切り替え",
"ko_KR": "전체 화면 전환",
"no_NO": "Fullskjermsvisning av/på",
"pl_PL": "Przełącz na tryb pełnoekranowy",
"pt_BR": "Mudar para Tela Cheia",
"ru_RU": "Переключить полноэкранный режим",
"sv_SE": "Växla helskärm",
"th_TH": "สลับเป็นโหมดเต็มหน้าจอ",
"tr_TR": "Tam Ekran Modunu Aç",
"uk_UA": "На весь екран",
"zh_CN": "切换全屏",
"zh_TW": "切換全螢幕模式"
}
},
{
"ID": "ShowConsoleButton",
"Translations": {
"ar_SA": "عرض وحدة التحكم",
"de_DE": "Zeige Konsole",
"el_GR": "Εμφάνιση Κονσόλας",
"en_US": "Show Console",
"es_ES": "Mostrar Consola",
"fr_FR": "Afficher la Console",
"he_IL": "הצג שורת פקודות",
"it_IT": "Mostra console",
"ja_JP": "コンソールを表示",
"ko_KR": "콘솔 표시",
"no_NO": "Vis konsoll",
"pl_PL": "Pokaż Konsolę",
"pt_BR": "Exibir Console",
"ru_RU": "Показать консоль",
"sv_SE": "Visa konsoll",
"th_TH": "แสดง คอนโซล",
"tr_TR": "Konsol'u Göster",
"uk_UA": "Показати консоль",
"zh_CN": "显示控制台",
"zh_TW": "顯示控制台"
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
{
"Locales": [
{
"ID": "StartGamesInFullscreenCheckboxLabel",
"Translations": {
"ar_SA": "ابدأ الألعاب بملء الشاشة",
"de_DE": "Spiele im Vollbild starten",
"el_GR": "Εκκίνηση Παιχνιδιών σε Πλήρη Οθόνη",
"en_US": "Start Games in Fullscreen",
"es_ES": "Iniciar Juegos en Pantalla Completa",
"fr_FR": "Démarrer les Jeux en Plein Écran",
"he_IL": "התחל משחקים במסך מלא",
"it_IT": "Avvia i giochi a schermo intero",
"ja_JP": "全画面でゲームを開始",
"ko_KR": "전체 화면으로 게임 시작",
"no_NO": "Start spill i fullskjerm",
"pl_PL": "Uruchamiaj gry na pełnym ekranie",
"pt_BR": "Iniciar Jogos em Tela Cheia",
"ru_RU": "Запускать игры в полноэкранном режиме",
"sv_SE": "Starta spel i helskärm",
"th_TH": "เริ่มเกมแบบเต็มหน้าจอ",
"tr_TR": "Oyunları Tam Ekranda Başlat",
"uk_UA": "Запускати ігри в повноекранному режимі",
"zh_CN": "全屏启动游戏",
"zh_TW": "以全螢幕啟動遊戲"
}
},
{
"ID": "StartGamesWithoutUICheckboxLabel",
"Translations": {
"ar_SA": "",
"de_DE": "Benutzeroberfläche beim Spielstart ausblenden",
"el_GR": "",
"en_US": "Hide UI on Game Start",
"es_ES": "Ocultar la Interfaz al Iniciar el Juego",
"fr_FR": "Masquer lInterface au Lancement du Jeu",
"he_IL": "",
"it_IT": "Nascondi linterfaccia allavvio del gioco",
"ja_JP": "",
"ko_KR": "게임 시작 시 UI 숨기기",
"no_NO": "Skjul brukergrensesnitt ved spillstart",
"pl_PL": "",
"pt_BR": "Ocultar a interface ao iniciar o jogo",
"ru_RU": "Скрывать интерфейс при запуске игры",
"sv_SE": "Dölj användargränssnitt vid spelstart",
"th_TH": "ซ่อน UI เมื่อเริ่มเกม",
"tr_TR": "",
"uk_UA": "Приховувати інтерфейс під час запуску гри",
"zh_CN": "启动游戏时隐藏界面",
"zh_TW": "啟動遊戲時隱藏介面"
}
},
{
"ID": "StartGamesWithoutUICheckboxLabelToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Automatically hides Ryujinx's UI whenever a game launches. While in-game, press F4 to show the UI.",
"es_ES": "",
"fr_FR": "Masque automatiquement l'interface de Ryujinx au lancement d'un jeu. En jeu, appuyez sur F4 pour afficher l'interface.",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "Автоматически скрывает интерфейс Ryujinx при запуске игры. Во время игры нажмите F4, чтобы показать интерфейс.",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
}
]
}

View File

@@ -1,23 +1,11 @@
#!/bin/sh
#!/usr/bin/env sh
SCRIPT_DIR=$(dirname "$(realpath "$0")")
if [ -f "$SCRIPT_DIR/Ryujinx.Headless.SDL3" ]; then
RYUJINX_BIN="Ryujinx.Headless.SDL3"
fi
if [ -f "$SCRIPT_DIR/Ryujinx" ]; then
RYUJINX_BIN="Ryujinx"
fi
if [ -z "$RYUJINX_BIN" ]; then
exit 1
fi
COMMAND="env LANG=C.UTF-8 DOTNET_EnableAlternateStackCheck=1"
if command -v gamemoderun > /dev/null 2>&1; then
COMMAND="$COMMAND gamemoderun"
fi
exec $COMMAND "$SCRIPT_DIR/$RYUJINX_BIN" "$@"
exec $COMMAND "$SCRIPT_DIR/Ryujinx" "$@"

View File

@@ -1,14 +1,12 @@
[Desktop Entry]
Version=1.0
Version=1.5
Name=Ryujinx
Type=Application
Icon=Ryujinx
Icon=app.ryujinx.Ryujinx
Exec=Ryujinx.sh %f
Comment=A Nintendo Switch Emulator
GenericName=Nintendo Switch Emulator
Terminal=false
Categories=Game;Emulator;
MimeType=application/x-nx-nca;application/x-nx-nro;application/x-nx-nso;application/x-nx-nsp;application/x-nx-xci;
Keywords=Switch;Nintendo;Emulator;
StartupWMClass=Ryujinx
PrefersNonDefaultGPU=true

View File

@@ -1,3 +0,0 @@
#!/bin/sh
CURRENTDIR="$(readlink -f "$(dirname "$0")")"
exec "$CURRENTDIR"/usr/bin/Ryujinx.sh "$@"

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>app.ryujinx.Ryujinx</id>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<name>Ryujinx</name>
<summary>Nintendo Switch 1 emulator written in C#</summary>
<description>
<p>Ryujinx is an open-source Nintendo Switch emulator, originally created by gdkchan, written in C#.</p>
</description>
<launchable type="desktop-id">app.ryujinx.Ryujinx.desktop</launchable>
<url type="homepage">https://ryujinx.app/</url>
<developer id="app.ryujinx">
<name>Ryubing</name>
</developer>
<screenshots>
<screenshot type="default">
<image>https://git.ryujinx.app/projects/Ryubing/raw/branch/master/docs/shell.png</image>
</screenshot>
</screenshots>
<provides>
<id>app.ryujinx.Ryujinx.desktop</id>
</provides>
<content_rating type="oars-1.1" />
</component>

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env sh
set -eu
ROOTDIR="$(readlink -f "$(dirname "$0")")"/../../../
@@ -7,20 +7,33 @@ cd "$ROOTDIR"
BUILDDIR=${BUILDDIR:-publish}
OUTDIR=${OUTDIR:-publish_appimage}
# AppStream
rm -rf AppDir
mkdir -p AppDir/usr/bin
mkdir -p AppDir/usr/lib AppDir/usr/bin
mkdir -p AppDir/usr/share/metainfo AppDir/usr/share/applications
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps/
cp distribution/linux/Ryujinx.desktop AppDir/Ryujinx.desktop
cp distribution/linux/appimage/AppRun AppDir/AppRun
cp distribution/misc/Logo.svg AppDir/Ryujinx.svg
cp -r "$BUILDDIR"/* AppDir/usr/lib/
cp distribution/linux/appimage/app.ryujinx.Ryujinx.appdata.xml AppDir/usr/share/metainfo/app.ryujinx.Ryujinx.appdata.xml
cp distribution/linux/app.ryujinx.Ryujinx.desktop AppDir/usr/share/applications/app.ryujinx.Ryujinx.desktop
cp distribution/misc/Logo.png AppDir/usr/share/icons/hicolor/256x256/apps/app.ryujinx.Ryujinx.png
ln -s ../lib/Ryujinx AppDir/usr/bin/Ryujinx
cp -r "$BUILDDIR"/* AppDir/usr/bin/
# AppImage Root
ln -s ./usr/share/applications/app.ryujinx.Ryujinx.desktop AppDir/app.ryujinx.Ryujinx.desktop
ln -s ./usr/share/icons/hicolor/256x256/apps/app.ryujinx.Ryujinx.png AppDir/.DirIcon
ln -s ./usr/share/icons/hicolor/256x256/apps/app.ryujinx.Ryujinx.png AppDir/app.ryujinx.Ryujinx.png
ln -s ./usr/lib/Ryujinx.sh AppDir/AppRun
# Ensure necessary bins are set as executable
chmod +x AppDir/AppRun AppDir/usr/bin/Ryujinx*
mkdir -p "$OUTDIR"
# The "-n" flag removes the appstream checks during build, in case the main website is down.
# Run "appstreamcli validate --explain AppDir/usr/share/metainfo/app.ryujinx.Ryujinx.appdata.xml" to check manually
appimagetool --appimage-extract-and-run -n --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 21 \
AppDir "$OUTDIR"/Ryujinx.AppImage
AppDir "$OUTDIR"/Ryujinx.AppImage

BIN
distribution/misc/Logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -110,7 +110,7 @@ namespace ARMeilleure.Translation.PTC
Profiler.Wait();
Profiler.ClearEntries();
Logger.Info?.Print(LogClass.Ptc, $"Initializing Profiled Persistent Translation Cache (enabled: {enabled}).");
Logger.Info?.Print(LogClass.Ptc, $"Initializing Profiled Persistent Translation Cache v{InternalVersion}\n\t\t (title: {titleIdText}, version: '{displayVersion}', selector: '{cacheSelector}', enabled: {enabled}).");
if (!enabled || string.IsNullOrEmpty(titleIdText) || titleIdText == TitleIdTextDefault)
{
@@ -129,8 +129,6 @@ namespace ARMeilleure.Translation.PTC
DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault;
_memoryMode = memoryMode;
Logger.Info?.Print(LogClass.Ptc, $"PPTC (v{InternalVersion}) Profile: {DisplayVersion}-{cacheSelector}");
string workPathActual = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", ActualDir);
string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", BackupDir);

View File

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

View File

@@ -0,0 +1,142 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid
{
[JsonConverter(typeof(JsonStringEnumConverter<PhysicalKey>))]
public enum PhysicalKey
{
Unknown,
ShiftLeft,
ShiftRight,
ControlLeft,
ControlRight,
AltLeft,
AltRight,
WinLeft,
WinRight,
Menu,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
F25,
F26,
F27,
F28,
F29,
F30,
F31,
F32,
F33,
F34,
F35,
Up,
Down,
Left,
Right,
Enter,
Escape,
Space,
Tab,
BackSpace,
Insert,
Delete,
PageUp,
PageDown,
Home,
End,
CapsLock,
ScrollLock,
PrintScreen,
Pause,
NumLock,
Clear,
Keypad0,
Keypad1,
Keypad2,
Keypad3,
Keypad4,
Keypad5,
Keypad6,
Keypad7,
Keypad8,
Keypad9,
KeypadDivide,
KeypadMultiply,
KeypadSubtract,
KeypadAdd,
KeypadDecimal,
KeypadEnter,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
Number0,
Number1,
Number2,
Number3,
Number4,
Number5,
Number6,
Number7,
Number8,
Number9,
Tilde,
Grave,
Minus,
Plus,
BracketLeft,
BracketRight,
Semicolon,
Quote,
Comma,
Period,
Slash,
BackSlash,
Unbound,
Count,
}
}

View File

@@ -24,6 +24,9 @@ namespace Ryujinx.Common.Helper
public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows());
// NOTE: On macOS, users have a more robust file association system (via Right-Click > Get Info > "Open with:" > Ryujinx > "Change All...)
// Custom file association isn't strictly necessary and will not provide any additional benefit to macOS users.
public static bool AreMimeTypesRegistered
{
get
@@ -38,8 +41,6 @@ namespace Ryujinx.Common.Helper
return AreMimeTypesRegisteredWindows();
}
// TODO: Add macOS support.
return false;
}
}
@@ -173,8 +174,6 @@ namespace Ryujinx.Common.Helper
return InstallWindowsMimeTypes();
}
// TODO: Add macOS support.
return false;
}
@@ -190,8 +189,6 @@ namespace Ryujinx.Common.Helper
return InstallWindowsMimeTypes(true);
}
// TODO: Add macOS support.
return false;
}
}

View File

@@ -147,6 +147,7 @@ namespace Ryujinx.Common
"0100c1f0051b6000", // Donkey Kong Country: Tropical Freeze
"0100ed000d390000", // Dr. Kawashima's Brain Training
"010067b017588000", // Endless Ocean Luminous
"01003da010e8a000", // Miitopia
"01006b5012b32000", // Part Time UFO
"0100704000B3A000", // Snipperclips
"01006a800016e000", // Super Smash Bros. Ultimate

View File

@@ -53,7 +53,7 @@ namespace Ryujinx.Cpu.Signal
public SignalHandlerRangeArray Ranges;
}
static class NativeSignalHandler
public static class NativeSignalHandler
{
private static readonly nint _handlerConfig;
private static nint _signalHandlerPtr;

View File

@@ -588,8 +588,10 @@ namespace Ryujinx.Graphics.Vulkan
dynamicStates[5] = DynamicState.StencilReference;
dynamicStates[6] = DynamicState.BlendConstants;
if (supportsExtDynamicState)
if (supportsExtDynamicState && (gd.SupportsMTL31 || !gd.IsMoltenVk))
{
// Requires Metal 3.1 and new MoltenVK, however extended dynamic states extension is not
// available on older versions of MVK, so we can safely check only OS version.
dynamicStates[dynamicStatesCount++] = DynamicState.VertexInputBindingStrideExt;
}

View File

@@ -52,7 +52,7 @@
<ItemGroup>
<PackageReference Include="OpenTK.Windowing.GraphicsLibraryFramework" />
<PackageReference Include="shaderc.net" />
<PackageReference Include="Silk.NET.Shaderc" ExcludeAssets="native" />
<PackageReference Include="Silk.NET.Vulkan" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />

View File

@@ -1,22 +1,17 @@
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
using shaderc;
using Silk.NET.Shaderc;
using Silk.NET.Vulkan;
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Result = shaderc.Result;
namespace Ryujinx.Graphics.Vulkan
{
class Shader : IDisposable
{
// The shaderc.net dependency's Options constructor and dispose are not thread safe.
// Take this lock when using them.
private static readonly Lock _shaderOptionsLock = new();
private static readonly nint _ptrMainEntryPointName = Marshal.StringToHGlobalAnsi("main");
private readonly Vk _api;
@@ -75,38 +70,33 @@ namespace Ryujinx.Graphics.Vulkan
private unsafe static byte[] GlslToSpirv(string glsl, ShaderStage stage)
{
Options options;
Shaderc api = Shaderc.GetApi();
Compiler* compiler = api.CompilerInitialize();
CompileOptions* options = api.CompileOptionsInitialize();
lock (_shaderOptionsLock)
api.CompileOptionsSetSourceLanguage(options, SourceLanguage.Glsl);
api.CompileOptionsSetTargetSpirv(options, SpirvVersion.Shaderc15);
api.CompileOptionsSetTargetEnv(options, TargetEnv.Vulkan, Vk.Version12);
CompilationResult* scr = api.CompileIntoSpv(compiler, glsl, (nuint)glsl.Length, GetShaderCShaderStage(stage), "Ryu", "main", options);
CompilationStatus status = api.ResultGetCompilationStatus(scr);
if (status != CompilationStatus.Success)
{
options = new Options(false)
{
SourceLanguage = SourceLanguage.Glsl,
TargetSpirVVersion = new SpirVVersion(1, 5),
};
}
options.SetTargetEnvironment(TargetEnvironment.Vulkan, EnvironmentVersion.Vulkan_1_2);
Compiler compiler = new(options);
Result scr = compiler.Compile(glsl, "Ryu", GetShaderCShaderStage(stage));
lock (_shaderOptionsLock)
{
options.Dispose();
}
if (scr.Status != Status.Success)
{
Logger.Error?.Print(LogClass.Gpu, $"Shader compilation error: {scr.Status} {scr.ErrorMessage}");
Logger.Error?.Print(LogClass.Gpu, $"Shader compilation error: {status} {api.ResultGetErrorMessageS(scr)}");
return null;
}
Span<byte> spirvBytes = new((void*)scr.CodePointer, (int)scr.CodeLength);
Span<byte> spirvBytes = new(api.ResultGetBytes(scr), (int)api.ResultGetLength(scr));
byte[] code = new byte[(scr.CodeLength + 3) & ~3];
byte[] code = new byte[(spirvBytes.Length + 3) & ~3];
spirvBytes.CopyTo(code.AsSpan()[..(int)scr.CodeLength]);
spirvBytes.CopyTo(code.AsSpan()[..spirvBytes.Length]);
api.CompilerRelease(compiler);
api.CompileOptionsRelease(options);
return code;
}

View File

@@ -98,6 +98,7 @@ namespace Ryujinx.Graphics.Vulkan
internal bool IsIntelArc { get; private set; }
internal bool IsQualcommProprietary { get; private set; }
internal bool IsMoltenVk { get; private set; }
internal bool SupportsMTL31 { get; private set; }
internal bool IsTBDR { get; private set; }
internal bool IsSharedMemory { get; private set; }
@@ -123,6 +124,8 @@ namespace Ryujinx.Graphics.Vulkan
// Any device running on MacOS is using MoltenVK, even Intel and AMD vendors.
if (IsMoltenVk = OperatingSystem.IsMacOS())
MVKInitialization.Initialize();
SupportsMTL31 = OperatingSystem.IsMacOSVersionAtLeast(14);
}
public static VulkanRenderer Create(

View File

@@ -443,6 +443,7 @@ namespace Ryujinx.HLE.HOS.Applets
if ((newCalc.Flags & KeyboardCalcFlags.SetInputText) != 0)
{
_textValue = newCalc.InputText;
_cursorBegin = _textValue.Length;
updateText = true;
Logger.Debug?.Print(LogClass.ServiceAm, $"Input text set to {_textValue}");

View File

@@ -118,7 +118,9 @@ namespace Ryujinx.HLE.HOS
}
}
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, _diskCacheSelector ?? "default");
string cacheSelector = _diskCacheSelector ?? "default";
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, cacheSelector);
return processContext;
}

View File

@@ -16,6 +16,7 @@ namespace Ryujinx.HLE.HOS
public IpcMessage Response { get; }
public BinaryReader RequestData { get; }
public BinaryWriter ResponseData { get; }
public ulong ClientProcessId => Request.HandleDesc is { HasPId: true } ? Request.HandleDesc.PId : Process.Pid;
public ServiceCtx(
Switch device,

View File

@@ -190,7 +190,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
// TODO: Account actually calls nn::arp::detail::IReader::GetApplicationControlProperty() with the current Pid and store the result (NACP file) internally.
// But since we use LibHac and we load one Application at a time, it's not necessary.
context.ResponseData.Write((byte)context.Device.Processes.ActiveApplication.ApplicationControlProperties.UserAccountSwitchLock);
context.ResponseData.Write((byte)context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties.UserAccountSwitchLock);
Logger.Stub?.PrintStub(LogClass.ServiceAcc);

View File

@@ -79,7 +79,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService
// OpenLibraryAppletSelfAccessor() -> object<nn::am::service::ILibraryAppletSelfAccessor>
public ResultCode OpenLibraryAppletSelfAccessor(ServiceCtx context)
{
MakeObject(context, new ILibraryAppletSelfAccessor(context));
MakeObject(context, new ILibraryAppletSelfAccessor(context, _pid));
return ResultCode.Success;
}

View File

@@ -8,9 +8,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
{
private readonly AppletStandalone _appletStandalone = new();
public ILibraryAppletSelfAccessor(ServiceCtx context)
public ILibraryAppletSelfAccessor(ServiceCtx context, ulong pid)
{
if (context.Device.Processes.ActiveApplication.ProgramId == 0x0100000000001009)
ulong programId = context.Device.Processes.GetProcess(pid).ProgramId;
if (programId == 0x0100000000001009)
{
// Create MiiEdit data.
_appletStandalone = new AppletStandalone()
@@ -26,7 +28,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
}
else
{
throw new NotImplementedException($"{context.Device.Processes.ActiveApplication.ProgramId} applet is not implemented.");
throw new NotImplementedException($"{programId} applet is not implemented.");
}
}

View File

@@ -44,8 +44,9 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
private int _jitLoaded;
private readonly LibHac.HorizonClient _horizon;
private readonly ulong _pid;
public IApplicationFunctions(Horizon system)
public IApplicationFunctions(Horizon system, ulong pid)
{
// TODO: Find where they are signaled.
_gpuErrorDetectedSystemEvent = new KEvent(system.KernelContext);
@@ -55,6 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
_unknownEvent = new KEvent(system.KernelContext);
_horizon = system.LibHacHorizonManager.AmClient;
_pid = pid;
}
[CommandCmif(1)]
@@ -115,11 +117,12 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
public ResultCode EnsureSaveData(ServiceCtx context)
{
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
var process = context.Device.Processes.GetProcess(_pid);
// Mask out the low nibble of the program ID to get the application ID
ApplicationId applicationId = new(context.Device.Processes.ActiveApplication.ProgramId & ~0xFul);
ApplicationId applicationId = new(process.Identity.ApplicationId);
ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
ApplicationControlProperty nacp = process.ApplicationControlProperties;
LibHac.HorizonClient hos = context.Device.System.LibHacHorizonManager.AmClient;
LibHac.Result result = hos.Fs.EnsureApplicationSaveData(out long requiredSize, applicationId, in nacp, in userId);
@@ -139,7 +142,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
// TODO: When above calls are implemented, switch to using ns:am
long desiredLanguageCode = context.Device.System.State.DesiredLanguageCode;
int supportedLanguages = (int)context.Device.Processes.ActiveApplication.ApplicationControlProperties.SupportedLanguageFlag;
int supportedLanguages = (int)context.Device.Processes.GetProcess(_pid).ApplicationControlProperties.SupportedLanguageFlag;
int firstSupported = BitOperations.TrailingZeroCount(supportedLanguages);
if (firstSupported > (int)TitleLanguage.BrazilianPortuguese)
@@ -182,7 +185,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
public ResultCode GetDisplayVersion(ServiceCtx context)
{
// If an NACP isn't found, the buffer will be all '\0' which seems to be the correct implementation.
context.ResponseData.Write(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion);
context.ResponseData.Write(context.Device.Processes.GetProcess(_pid).ApplicationControlProperties.DisplayVersion);
return ResultCode.Success;
}
@@ -235,11 +238,12 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
ushort index = (ushort)context.RequestData.ReadUInt64();
long saveSize = context.RequestData.ReadInt64();
long journalSize = context.RequestData.ReadInt64();
var process = context.Device.Processes.GetProcess(_pid);
// Mask out the low nibble of the program ID to get the application ID
ApplicationId applicationId = new(context.Device.Processes.ActiveApplication.ProgramId & ~0xFul);
ApplicationId applicationId = new(process.Identity.ApplicationId);
ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
ApplicationControlProperty nacp = process.ApplicationControlProperties;
LibHac.Result result = _horizon.Fs.CreateApplicationCacheStorage(out long requiredSize,
out CacheStorageTargetMedia storageTarget, applicationId, in nacp, index, saveSize, journalSize);

View File

@@ -70,7 +70,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService
// GetApplicationFunctions() -> object<nn::am::service::IApplicationFunctions>
public ResultCode GetApplicationFunctions(ServiceCtx context)
{
MakeObject(context, new IApplicationFunctions(context.Device.System));
MakeObject(context, new IApplicationFunctions(context.Device.System, _pid));
return ResultCode.Success;
}

View File

@@ -27,13 +27,18 @@ namespace Ryujinx.HLE.HOS.Services.Arp
}
public static ApplicationLaunchProperty GetByPid(ServiceCtx context)
{
return GetByPid(context, context.ClientProcessId);
}
public static ApplicationLaunchProperty GetByPid(ServiceCtx context, ulong pid)
{
// TODO: Handle ApplicationLaunchProperty as array when pid will be supported and return the right item.
// For now we can hardcode values, and fix it after GetApplicationLaunchProperty is implemented.
return new ApplicationLaunchProperty
{
TitleId = context.Device.Processes.ActiveApplication.ProgramId,
TitleId = context.Device.Processes.GetProcess(pid).ProgramId,
Version = 0x00,
BaseGameStorageId = (byte)StorageId.BuiltInSystem,
UpdateGameStorageId = (byte)StorageId.None,

View File

@@ -50,7 +50,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.GetProcess(context.ClientProcessId).ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
context.ResponseData.WriteStruct(applicationAlbumEntry);
@@ -98,7 +98,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.GetProcess(context.ClientProcessId).ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
context.ResponseData.WriteStruct(applicationAlbumEntry);
@@ -143,7 +143,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.GetProcess(context.ClientProcessId).ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
context.ResponseData.WriteStruct(applicationAlbumEntry);

View File

@@ -1,5 +1,6 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Fatal.Types;
using Ryujinx.HLE.Loaders.Processes;
using System;
using System.Runtime.InteropServices;
using System.Text;
@@ -50,12 +51,13 @@ namespace Ryujinx.HLE.HOS.Services.Fatal
private ResultCode ThrowFatalWithCpuContextImpl(ServiceCtx context, ResultCode resultCode, ulong pid, FatalPolicy fatalPolicy, ReadOnlySpan<byte> cpuContext)
{
ProcessResult process = context.Device.Processes.GetProcess(pid);
StringBuilder errorReport = new();
errorReport.AppendLine();
errorReport.AppendLine("ErrorReport log:");
errorReport.AppendLine($"\tTitleId: {context.Device.Processes.ActiveApplication.ProgramIdText}");
errorReport.AppendLine($"\tTitleId: {process.ProgramIdText}");
errorReport.AppendLine($"\tPid: {pid}");
errorReport.AppendLine($"\tResultCode: {((int)resultCode & 0x1FF) + 2000}-{((int)resultCode >> 9) & 0x3FFF:d4}");
errorReport.AppendLine($"\tFatalPolicy: {fatalPolicy}");
@@ -64,7 +66,7 @@ namespace Ryujinx.HLE.HOS.Services.Fatal
{
errorReport.AppendLine("CPU Context:");
if (context.Device.Processes.ActiveApplication.Is64Bit)
if (process.Is64Bit)
{
CpuContext64 cpuContext64 = MemoryMarshal.Cast<byte, CpuContext64>(cpuContext)[0];

View File

@@ -885,7 +885,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs
{
byte programIndex = context.RequestData.ReadByte();
if ((context.Device.Processes.ActiveApplication.ProgramId & 0xf) != programIndex)
if (context.Device.Processes.GetProcess(_pid).Identity.ProgramIndex != programIndex)
{
throw new NotImplementedException($"Accessing storage from other programs is not supported (program index = {programIndex}).");
}

View File

@@ -60,7 +60,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
private bool CheckLocalCommunicationIdPermission(ServiceCtx context, ulong localCommunicationIdChecked)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
ApplicationControlProperty controlProperty = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
foreach (ulong localCommunicationId in controlProperty.LocalCommunicationId)
{
@@ -438,7 +438,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
ApplicationControlProperty controlProperty = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
scanFilter.NetworkId.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
}
@@ -613,7 +613,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
ApplicationControlProperty controlProperty = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
networkConfig.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
}
@@ -948,7 +948,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
ApplicationControlProperty controlProperty = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
networkInfo.NetworkId.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
}
@@ -1208,7 +1208,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
// TODO: Call nn::arp::GetApplicationLaunchProperty here when implemented.
NetworkClient.SetGameVersion(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion);
NetworkClient.SetGameVersion(context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties.DisplayVersion);
resultCode = ResultCode.Success;
_nifmResultCode = resultCode;

View File

@@ -50,7 +50,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
return CountAddOnContentImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
return CountAddOnContentImpl(context, context.Device.Processes.GetProcess(pid).ProgramId);
}
[CommandCmif(3)]
@@ -63,7 +63,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
return ListAddContentImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
return ListAddContentImpl(context, context.Device.Processes.GetProcess(pid).ProgramId);
}
[CommandCmif(4)] // 1.0.0-6.2.0
@@ -85,7 +85,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
return GetAddOnContentBaseIdImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
return GetAddOnContentBaseIdImpl(context, context.Device.Processes.GetProcess(pid).ProgramId);
}
[CommandCmif(6)] // 1.0.0-6.2.0
@@ -107,7 +107,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
return PrepareAddOnContentImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
return PrepareAddOnContentImpl(context, context.Device.Processes.GetProcess(pid).ProgramId);
}
[CommandCmif(8)] // 4.0.0+
@@ -138,7 +138,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
// TODO: Found where stored value is used.
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Processes.ActiveApplication.ProgramId);
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Processes.GetProcess(pid).ProgramId);
if (resultCode != ResultCode.Success)
{
@@ -310,7 +310,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
// NOTE: Service calls arp:r GetApplicationControlProperty to get AddOnContentBaseId using TitleId,
// If the call fails, it returns ResultCode.InvalidPid.
_addOnContentBaseId = context.Device.Processes.ActiveApplication.ApplicationControlProperties.AddOnContentBaseId;
_addOnContentBaseId = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties.AddOnContentBaseId;
if (_addOnContentBaseId == 0)
{
@@ -324,7 +324,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
{
uint index = context.RequestData.ReadUInt32();
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Processes.ActiveApplication.ProgramId);
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, titleId);
if (resultCode != ResultCode.Success)
{

View File

@@ -19,7 +19,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns
ulong position = context.Request.ReceiveBuff[0].Position;
ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
ApplicationControlProperty nacp = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
context.Memory.Write(position, SpanHelpers.AsByteSpan(ref nacp).ToArray());

View File

@@ -18,7 +18,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns
ulong position = context.Request.ReceiveBuff[0].Position;
ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
ApplicationControlProperty nacp = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
context.Memory.Write(position, SpanHelpers.AsByteSpan(ref nacp).ToArray());

View File

@@ -48,21 +48,22 @@ namespace Ryujinx.HLE.HOS.Services.Pctl.ParentalControlServiceFactory
{
if ((_permissionFlag & 0x40) == 0)
{
ulong titleId = ApplicationLaunchProperty.GetByPid(context).TitleId;
ulong titleId = ApplicationLaunchProperty.GetByPid(context, _pid).TitleId;
if (titleId != 0)
{
_titleId = titleId;
var process = context.Device.Processes.GetProcess(_pid);
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented, if it return ResultCode.Success we assign fields.
_ratingAge = new int[context.Device.Processes.ActiveApplication.ApplicationControlProperties.RatingAge.Length];
_ratingAge = new int[process.ApplicationControlProperties.RatingAge.Length];
for (int i = 0; i < _ratingAge.Length; i++)
{
_ratingAge[i] = Convert.ToInt32(context.Device.Processes.ActiveApplication.ApplicationControlProperties.RatingAge[i]);
_ratingAge[i] = Convert.ToInt32(process.ApplicationControlProperties.RatingAge[i]);
}
_parentalControlFlag = context.Device.Processes.ActiveApplication.ApplicationControlProperties.ParentalControlFlag;
_parentalControlFlag = process.ApplicationControlProperties.ParentalControlFlag;
}
}

View File

@@ -2,6 +2,7 @@ using Ryujinx.Common;
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types;
using Ryujinx.HLE.Loaders.Processes;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -31,7 +32,8 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
}
}
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.Processes.ActiveApplication.ApplicationControlProperties.PlayLogQueryCapability;
ProcessResult process = context.Device.Processes.GetProcess(context.ClientProcessId);
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)process.ApplicationControlProperties.PlayLogQueryCapability;
List<ulong> titleIds = [];
@@ -45,7 +47,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
// Check if input title ids are in the whitelist.
foreach (ulong titleId in titleIds)
{
if (!context.Device.Processes.ActiveApplication.ApplicationControlProperties.PlayLogQueryableApplicationId.AsReadOnlySpan().Contains(titleId))
if (!process.ApplicationControlProperties.PlayLogQueryableApplicationId.AsReadOnlySpan().Contains(titleId))
{
return (ResultCode)Am.ResultCode.ObjectInvalid;
}

View File

@@ -0,0 +1,34 @@
namespace Ryujinx.HLE.Loaders.Processes
{
public readonly struct ProcessIdentity
{
public ulong ProcessId { get; }
public ulong ProgramId { get; }
public ulong ApplicationId { get; }
public byte ProgramIndex { get; }
public string ProgramIdText { get; }
public string DisplayVersion { get; }
public ProcessKind Kind { get; }
public ProcessIdentity(
ulong processId,
ulong programId,
byte programIndex,
string displayVersion,
ProcessKind kind)
{
ProcessId = processId;
ProgramId = programId;
ProgramIndex = programIndex;
ApplicationId = programId & ~0xFul;
ProgramIdText = $"{programId:x16}";
DisplayVersion = displayVersion ?? string.Empty;
Kind = kind;
}
public override string ToString()
{
return $"{Kind} pid={ProcessId} program={ProgramIdText} application={ApplicationId:x16} index={ProgramIndex} version={DisplayVersion}";
}
}
}

View File

@@ -0,0 +1,12 @@
namespace Ryujinx.HLE.Loaders.Processes
{
public enum ProcessKind
{
Unknown,
Application,
SystemApplication,
SystemApplet,
LibraryApplet,
Homebrew,
}
}

View File

@@ -93,6 +93,23 @@ namespace Ryujinx.HLE.Loaders.Processes
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
}
public bool TryGetProcess(ulong pid, out ProcessResult process)
{
return _processesByPid.TryGetValue(pid, out process);
}
public ProcessResult GetProcess(ulong pid)
{
if (_processesByPid.TryGetValue(pid, out ProcessResult process))
{
return process;
}
Logger.Warning?.Print(LogClass.Loader, $"Process metadata for pid {pid} was not found. Falling back to active application metadata.");
return ActiveApplication;
}
public bool LoadXci(string path, ulong applicationId)
{
FileStream stream = new(path, FileMode.Open, FileAccess.Read);

View File

@@ -436,6 +436,7 @@ namespace Ryujinx.HLE.Loaders.Processes
allowCodeMemoryForJit,
processContextFactory.DiskCacheLoadState,
process.Pid,
programIndex,
meta.MainThreadPriority,
meta.MainThreadStackSize,
device.System.State.DesiredTitleLanguage);

View File

@@ -10,7 +10,7 @@ namespace Ryujinx.HLE.Loaders.Processes
{
public class ProcessResult
{
public static ProcessResult Failed => new(null, new BlitStruct<ApplicationControlProperty>(1), false, false, null, 0, 0, 0, TitleLanguage.AmericanEnglish);
public static ProcessResult Failed => new(null, new BlitStruct<ApplicationControlProperty>(1), false, false, null, 0, 0, 0, 0, TitleLanguage.AmericanEnglish);
private readonly byte _mainThreadPriority;
private readonly uint _mainThreadStackSize;
@@ -28,6 +28,7 @@ namespace Ryujinx.HLE.Loaders.Processes
public readonly bool Is64Bit;
public readonly bool DiskCacheEnabled;
public readonly bool AllowCodeMemoryForJit;
public readonly ProcessIdentity Identity;
public ProcessResult(
MetaLoader metaLoader,
@@ -36,6 +37,7 @@ namespace Ryujinx.HLE.Loaders.Processes
bool allowCodeMemoryForJit,
IDiskCacheLoadState diskCacheLoadState,
ulong pid,
byte programIndex,
byte mainThreadPriority,
uint mainThreadStackSize,
TitleLanguage titleLanguage)
@@ -71,6 +73,7 @@ namespace Ryujinx.HLE.Loaders.Processes
ProgramId = programId;
ProgramIdText = $"{programId:x16}";
Is64Bit = metaLoader.IsProgram64Bit;
Identity = new ProcessIdentity(pid, programId, programIndex, DisplayVersion, GetProcessKind(programId));
}
else
@@ -84,6 +87,31 @@ namespace Ryujinx.HLE.Loaders.Processes
AllowCodeMemoryForJit = allowCodeMemoryForJit;
}
private static ProcessKind GetProcessKind(ulong programId)
{
if (programId == 0)
{
return ProcessKind.Application;
}
if (programId is >= 0x0100000000001000 and <= 0x0100000000001FFF)
{
return ProcessKind.SystemApplet;
}
if (programId is >= 0x0100000000000800 and <= 0x0100000000000FFF)
{
return ProcessKind.LibraryApplet;
}
if (programId <= 0x0100000000007FFF)
{
return ProcessKind.SystemApplication;
}
return ProcessKind.Application;
}
public bool Start(Switch device)
{
device.Configuration.ContentManager.LoadEntries(device);
@@ -109,6 +137,7 @@ namespace Ryujinx.HLE.Loaders.Processes
: device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?";
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]");
Logger.Info?.Print(LogClass.Loader, $"Process identity: {Identity}");
return true;
}

View File

@@ -155,9 +155,7 @@ namespace Ryujinx.Input.SDL3
result |= GamepadFeaturesFlag.Led;
}
SDL_UnlockProperties(propID);
// NOTE: Do not call SDL_DestroyProperties here. These properties are owned
// internally by SDL and are freed when SDL_CloseGamepad is called (in Dispose).
SDL_DestroyProperties(propID);
return result;
}
@@ -165,6 +163,9 @@ namespace Ryujinx.Input.SDL3
public string Id { get; }
public string Name { get; }
// Expose vendor id for higher-fidelity device detection in UI
public ushort VendorId => _gamepadHandle != null ? SDL_GetGamepadVendor(_gamepadHandle) : (ushort)0;
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
protected virtual void Dispose(bool disposing)

View File

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

View File

@@ -9,25 +9,15 @@ using System.Runtime.CompilerServices;
using System.Threading;
using SDL;
using static SDL.SDL3;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input.SDL3
{
class SDL3Keyboard : IKeyboard
{
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, Key From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not Key.Unbound;
}
private readonly Lock _userMappingLock = new();
#pragma warning disable IDE0052 // Remove unread private member
private readonly SDL3KeyboardDriver _driver;
#pragma warning restore IDE0052
private StandardKeyboardInputConfig _configuration;
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private static readonly SDL_Keycode[] _keysDriverMapping =
@@ -172,9 +162,8 @@ namespace Ryujinx.Input.SDL3
SDL_Keycode.SDLK_0
];
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
public SDL3Keyboard(string id, string name)
{
_driver = driver;
Id = id;
Name = name;
_buttonsUserMapping = [];
@@ -196,9 +185,9 @@ namespace Ryujinx.Input.SDL3
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe static int ToSDL3Scancode(Key key)
private unsafe static int ToSDL3Scancode(ConfigPhysicalKey key)
{
if (key is >= Key.Unknown and <= Key.Menu)
if (key is >= ConfigPhysicalKey.Unknown and <= ConfigPhysicalKey.Menu)
{
return -1;
}
@@ -206,18 +195,18 @@ namespace Ryujinx.Input.SDL3
return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null);
}
private static SDL_Keymod GetKeyboardModifierMask(Key key)
private static SDL_Keymod GetKeyboardModifierMask(ConfigPhysicalKey key)
{
return key switch
{
Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
Key.AltRight => SDL_Keymod.SDL_KMOD_RALT,
Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
ConfigPhysicalKey.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
ConfigPhysicalKey.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
ConfigPhysicalKey.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
ConfigPhysicalKey.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
ConfigPhysicalKey.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
ConfigPhysicalKey.AltRight => SDL_Keymod.SDL_KMOD_RALT,
ConfigPhysicalKey.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
ConfigPhysicalKey.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
// NOTE: Menu key isn't supported by SDL3.
_ => SDL_Keymod.SDL_KMOD_NONE
};
@@ -233,9 +222,9 @@ namespace Ryujinx.Input.SDL3
rawKeyboardState = SDL_GetKeyboardState(null);
}
bool[] keysState = new bool[(int)Key.Count];
bool[] keysState = new bool[(int)ConfigPhysicalKey.Count];
for (Key key = 0; key < Key.Count; key++)
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
{
int index = ToSDL3Scancode(key);
if (index == -1)
@@ -265,36 +254,6 @@ namespace Ryujinx.Input.SDL3
return value * ConvertRate;
}
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
{
short stickX = 0;
short stickY = 0;
if (snapshot.IsPressed((Key)stickConfig.StickUp))
{
stickY += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickDown))
{
stickY -= 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickRight))
{
stickX += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
{
stickX -= 1;
}
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
@@ -307,9 +266,9 @@ namespace Ryujinx.Input.SDL3
return result;
}
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
{
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
if (!entry.IsValid)
{
continue;
}
@@ -321,8 +280,8 @@ namespace Ryujinx.Input.SDL3
}
}
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -358,38 +317,15 @@ namespace Ryujinx.Input.SDL3
{
_configuration = (StandardKeyboardInputConfig)configuration;
// First clear the buttons mapping
_buttonsUserMapping.Clear();
// Then configure left joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
// Finally configure right joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
}
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
Logger.Debug?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
}
public void SetTriggerThreshold(float triggerThreshold)

View File

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

View File

@@ -1,3 +1,5 @@
using Ryujinx.Common.Logging;
namespace Ryujinx.Input.Assigner
{
/// <summary>
@@ -8,22 +10,40 @@ namespace Ryujinx.Input.Assigner
private readonly IKeyboard _keyboard;
private KeyboardStateSnapshot _keyboardState;
private Button? _pressedButton;
public KeyboardKeyAssigner(IKeyboard keyboard)
{
_keyboard = keyboard;
}
public void Initialize() { }
public void Initialize()
{
_pressedButton = null;
}
public void ReadInput()
{
_keyboardState = _keyboard.GetKeyboardStateSnapshot();
if (_pressedButton is null)
{
Button? buttonFromState = GetPressedButtonFromState();
Button? buttonFromBufferedPress = buttonFromState is null ? GetPressedButtonFromBufferedPress() : null;
_pressedButton = buttonFromState ?? buttonFromBufferedPress;
}
if (_pressedButton is not null)
{
string source = _pressedButton.HasValue && GetPressedButtonFromState() is not null ? "state" : "buffered-press";
Logger.Debug?.Print(LogClass.UI, $"Keyboard assigner registered key={_pressedButton.Value.AsHidType<Key>()}, source={source}, cancelPressed={ShouldCancel()}");
}
}
public bool IsAnyButtonPressed()
{
return GetPressedButton() is not null;
return _pressedButton is not null;
}
public bool ShouldCancel()
@@ -33,18 +53,53 @@ namespace Ryujinx.Input.Assigner
public Button? GetPressedButton()
{
Button? keyPressed = null;
return !ShouldCancel() ? _pressedButton : null;
}
private Button? GetPressedButtonFromState()
{
Key aliasedKey = GetAliasedPressedKey();
if (aliasedKey != Key.Unknown)
{
return new Button(aliasedKey);
}
for (Key key = Key.Unknown; key < Key.Count; key++)
{
if (_keyboardState.IsPressed(key))
{
keyPressed = new(key);
break;
return new Button(key);
}
}
return !ShouldCancel() ? keyPressed : null;
return null;
}
private Button? GetPressedButtonFromBufferedPress()
{
return _keyboard.TryConsumePressedKey(out Key key) ? new Button(key) : null;
}
private Key GetAliasedPressedKey()
{
// On some layouts (for example AltGr on Windows), Right Alt is reported as Ctrl+Alt.
// Prefer AltRight in that case so the binding reflects the physical key used.
if (_keyboardState.IsPressed(Key.ControlLeft) && _keyboardState.IsPressed(Key.AltRight))
{
return Key.AltRight;
}
// On some Copilot keyboards, the key in the right-control position is reported as
// ShiftLeft+Win+F23. Prefer ControlRight so the binding reflects that physical key.
if (_keyboardState.IsPressed(Key.ShiftLeft) &&
_keyboardState.IsPressed(Key.F23) &&
(_keyboardState.IsPressed(Key.WinLeft) || _keyboardState.IsPressed(Key.WinRight)))
{
return Key.ControlRight;
}
return Key.Unknown;
}
}
}

View File

@@ -2,6 +2,7 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
@@ -233,7 +234,9 @@ namespace Ryujinx.Input.HLE
_gamepad?.Dispose();
Id = config.Id;
_gamepad = GamepadDriver.GetGamepad(Id);
_gamepad = config is StandardKeyboardInputConfig && GamepadDriver is IKeyboardModeDriver keyboardModeDriver
? keyboardModeDriver.GetKeyboard(Id, KeyboardInputMode.Physical)
: GamepadDriver.GetGamepad(Id);
UpdateUserConfiguration(config);
@@ -558,24 +561,25 @@ namespace Ryujinx.Input.HLE
!controllerConfig.Rumble.EnableRumble)
{
return;
}
VibrationValue leftVibrationValue = dualVibrationValue.Item1;
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
if (!controllerConfig.Rumble.UseHDRumble || _gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false)
{
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15)));
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85)));
_gamepad?.Rumble(low, high, 0xFFFFFFFF);
}
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
// Value=value/multiplier * multiplier (result)
$"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " +
$"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeHigh}), " +

View File

@@ -24,7 +24,7 @@ namespace Ryujinx.Input.HLE
private readonly Lock _lock = new();
private bool _blockInputUpdates;
private int _inputUpdateBlockCount;
private const int MaxControllers = 9;
@@ -36,6 +36,7 @@ namespace Ryujinx.Input.HLE
private bool _isDisposed;
private List<InputConfig> _inputConfig;
private List<InputConfig> _requestedInputConfig;
private bool _enableKeyboard;
private bool _enableMouse;
private Switch _device;
@@ -52,6 +53,7 @@ namespace Ryujinx.Input.HLE
_gamepadDriver = gamepadDriver;
_mouseDriver = mouseDriver;
_inputConfig = [];
_requestedInputConfig = [];
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -89,29 +91,23 @@ namespace Ryujinx.Input.HLE
}
}
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
}
}
private void HandleOnGamepadConnected(string id)
private void HandleOnGamepadConnected(string _)
{
// Force input reload
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
{
IGamepadDriver targetDriver = _gamepadDriver;
if (config is StandardControllerInputConfig)
{
targetDriver = _gamepadDriver;
}
else if (config is StandardKeyboardInputConfig)
{
targetDriver = _keyboardDriver;
}
IGamepadDriver targetDriver =
config is StandardKeyboardInputConfig
? _keyboardDriver
: _gamepadDriver;
Debug.Assert(targetDriver != null, "Unknown input configuration!");
@@ -127,11 +123,13 @@ namespace Ryujinx.Input.HLE
{
lock (_lock)
{
_requestedInputConfig = inputConfig?.ToList() ?? [];
NpadController[] oldControllers = _controllers.ToArray();
List<InputConfig> validInputs = [];
foreach (InputConfig inputConfigEntry in inputConfig)
foreach (InputConfig inputConfigEntry in _requestedInputConfig)
{
NpadController controller;
int index = (int)inputConfigEntry.PlayerIndex;
@@ -147,7 +145,16 @@ namespace Ryujinx.Input.HLE
controller = new(_cemuHookClient);
}
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
InputConfig activeConfig = inputConfigEntry;
bool isValid = DriverConfigurationUpdate(ref controller, activeConfig);
if (!isValid &&
inputConfigEntry is StandardControllerInputConfig &&
TryGetKeyboardFallback(inputConfigEntry, out StandardKeyboardInputConfig fallbackConfig))
{
activeConfig = fallbackConfig;
isValid = DriverConfigurationUpdate(ref controller, activeConfig);
}
if (!isValid)
{
@@ -157,7 +164,7 @@ namespace Ryujinx.Input.HLE
else
{
_controllers[index] = controller;
validInputs.Add(inputConfigEntry);
validInputs.Add(activeConfig);
}
}
@@ -169,7 +176,7 @@ namespace Ryujinx.Input.HLE
oldControllers[i] = null;
}
_inputConfig = inputConfig;
_inputConfig = validInputs;
_enableKeyboard = enableKeyboard;
_enableMouse = enableMouse;
@@ -177,16 +184,58 @@ namespace Ryujinx.Input.HLE
}
}
private bool TryGetKeyboardFallback(InputConfig inputConfig, out StandardKeyboardInputConfig fallbackConfig)
{
fallbackConfig = null;
ReadOnlySpan<string> keyboardIds = _keyboardDriver.GamepadsIds;
if (keyboardIds.IsEmpty)
{
return false;
}
string keyboardId = keyboardIds[0];
using IGamepad keyboard = _keyboardDriver.GetGamepad(keyboardId);
if (keyboard == null)
{
return false;
}
fallbackConfig = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
keyboardId,
keyboard.Name,
inputConfig.ControllerType,
inputConfig.PlayerIndex);
return true;
}
private void ClearInputDriverStates()
{
foreach (InputConfig inputConfig in _inputConfig)
{
_controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear();
}
}
public void UnblockInputUpdates()
{
lock (_lock)
{
foreach (InputConfig inputConfig in _inputConfig)
if (_inputUpdateBlockCount == 0)
{
_controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear();
return;
}
_blockInputUpdates = false;
_inputUpdateBlockCount--;
if (_inputUpdateBlockCount == 0)
{
ClearInputDriverStates();
}
}
}
@@ -195,7 +244,7 @@ namespace Ryujinx.Input.HLE
get
{
lock (_lock)
return _blockInputUpdates;
return _inputUpdateBlockCount > 0;
}
}
@@ -203,7 +252,7 @@ namespace Ryujinx.Input.HLE
{
lock (_lock)
{
_blockInputUpdates = true;
_inputUpdateBlockCount++;
}
}
@@ -235,7 +284,7 @@ namespace Ryujinx.Input.HLE
bool isJoyconPair = false;
// Do we allow input updates and is a controller connected?
if (!_blockInputUpdates && controller != null)
if (_inputUpdateBlockCount == 0 && controller != null)
{
DriverConfigurationUpdate(ref controller, inputConfig);
@@ -273,7 +322,7 @@ namespace Ryujinx.Input.HLE
}
}
if (!_blockInputUpdates && _enableKeyboard)
if (_inputUpdateBlockCount == 0 && _enableKeyboard)
{
hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver);
}
@@ -334,7 +383,7 @@ namespace Ryujinx.Input.HLE
}
}
internal InputConfig GetPlayerInputConfigByIndex(int index)
public InputConfig GetPlayerInputConfigByIndex(int index)
{
lock (_lock)
{

View File

@@ -1,5 +1,6 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input
{
@@ -33,15 +34,26 @@ namespace Ryujinx.Input
{
if (_keyState is null)
{
_keyState = new bool[(int)Key.Count];
_keyState = new bool[(int)ConfigPhysicalKey.Count];
}
for (Key key = 0; key < Key.Count; key++)
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
{
_keyState[(int)key] = keyboard.IsPressed(key);
_keyState[(int)key] = keyboard.IsPressed((Key)(int)key);
}
return new KeyboardStateSnapshot(_keyState);
}
/// <summary>
/// Try to consume a recently pressed key.
/// </summary>
/// <param name="key">The pressed key, if available.</param>
/// <returns>True if a key press was consumed.</returns>
bool TryConsumePressedKey(out Key key)
{
key = Key.Unknown;
return false;
}
}
}

View File

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

View File

@@ -0,0 +1,148 @@
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 ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
namespace Ryujinx.Input
{
public static class InputConfigDefaults
{
public static StandardKeyboardInputConfig CreateDefaultKeyboardConfiguration(
string id,
string name,
ControllerType controllerType,
PlayerIndex playerIndex)
{
return new StandardKeyboardInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.WindowKeyboard,
Id = id,
Name = name,
PlayerIndex = playerIndex,
ControllerType = controllerType,
LeftJoycon = new LeftJoyconCommonConfig<ConfigPhysicalKey>
{
DpadUp = ConfigPhysicalKey.Up,
DpadDown = ConfigPhysicalKey.Down,
DpadLeft = ConfigPhysicalKey.Left,
DpadRight = ConfigPhysicalKey.Right,
ButtonMinus = ConfigPhysicalKey.Minus,
ButtonL = ConfigPhysicalKey.E,
ButtonZl = ConfigPhysicalKey.Q,
ButtonSl = ConfigPhysicalKey.Unbound,
ButtonSr = ConfigPhysicalKey.Unbound,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<ConfigPhysicalKey>
{
StickUp = ConfigPhysicalKey.W,
StickDown = ConfigPhysicalKey.S,
StickLeft = ConfigPhysicalKey.A,
StickRight = ConfigPhysicalKey.D,
StickButton = ConfigPhysicalKey.F,
},
RightJoycon = new RightJoyconCommonConfig<ConfigPhysicalKey>
{
ButtonA = ConfigPhysicalKey.Z,
ButtonB = ConfigPhysicalKey.X,
ButtonX = ConfigPhysicalKey.C,
ButtonY = ConfigPhysicalKey.V,
ButtonPlus = ConfigPhysicalKey.Plus,
ButtonR = ConfigPhysicalKey.U,
ButtonZr = ConfigPhysicalKey.O,
ButtonSl = ConfigPhysicalKey.Unbound,
ButtonSr = ConfigPhysicalKey.Unbound,
},
RightJoyconStick = new JoyconConfigKeyboardStick<ConfigPhysicalKey>
{
StickUp = ConfigPhysicalKey.I,
StickDown = ConfigPhysicalKey.K,
StickLeft = ConfigPhysicalKey.J,
StickRight = ConfigPhysicalKey.L,
StickButton = ConfigPhysicalKey.H,
},
};
}
public static StandardControllerInputConfig CreateDefaultControllerConfiguration(
string id,
string name,
ControllerType controllerType,
PlayerIndex playerIndex,
bool isNintendoStyle)
{
return new StandardControllerInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL3,
Id = id,
Name = name,
PlayerIndex = playerIndex,
ControllerType = controllerType,
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.SingleLeftTrigger0,
ButtonSr = ConfigGamepadInputId.SingleRightTrigger0,
},
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Left,
StickButton = ConfigGamepadInputId.LeftStick,
InvertStickX = false,
InvertStickY = false,
Rotate90CW = 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.SingleLeftTrigger1,
ButtonSr = ConfigGamepadInputId.SingleRightTrigger1,
},
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Right,
StickButton = ConfigGamepadInputId.RightStick,
InvertStickX = false,
InvertStickY = false,
Rotate90CW = false,
},
Motion = new StandardMotionConfigController
{
MotionBackend = MotionInputBackendType.GamepadDriver,
EnableMotion = true,
Sensitivity = 100,
GyroDeadzone = 1,
},
Rumble = new RumbleConfigController
{
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false,
UseHDRumble = true,
},
};
}
}
}

View File

@@ -0,0 +1,78 @@
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using System.Numerics;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input
{
public static class KeyboardInputMappingHelper
{
public readonly record struct KeyboardButtonMapping(GamepadButtonInputId To, ConfigPhysicalKey From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound;
}
public static KeyboardButtonMapping[] BuildButtonMappings(StandardKeyboardInputConfig configuration) =>
[
// Left JoyCon
new(GamepadButtonInputId.LeftStick, configuration.LeftJoyconStick.StickButton),
new(GamepadButtonInputId.DpadUp, configuration.LeftJoycon.DpadUp),
new(GamepadButtonInputId.DpadDown, configuration.LeftJoycon.DpadDown),
new(GamepadButtonInputId.DpadLeft, configuration.LeftJoycon.DpadLeft),
new(GamepadButtonInputId.DpadRight, configuration.LeftJoycon.DpadRight),
new(GamepadButtonInputId.Minus, configuration.LeftJoycon.ButtonMinus),
new(GamepadButtonInputId.LeftShoulder, configuration.LeftJoycon.ButtonL),
new(GamepadButtonInputId.LeftTrigger, configuration.LeftJoycon.ButtonZl),
new(GamepadButtonInputId.SingleRightTrigger0, configuration.LeftJoycon.ButtonSr),
new(GamepadButtonInputId.SingleLeftTrigger0, configuration.LeftJoycon.ButtonSl),
// Right JoyCon
new(GamepadButtonInputId.RightStick, configuration.RightJoyconStick.StickButton),
new(GamepadButtonInputId.A, configuration.RightJoycon.ButtonA),
new(GamepadButtonInputId.B, configuration.RightJoycon.ButtonB),
new(GamepadButtonInputId.X, configuration.RightJoycon.ButtonX),
new(GamepadButtonInputId.Y, configuration.RightJoycon.ButtonY),
new(GamepadButtonInputId.Plus, configuration.RightJoycon.ButtonPlus),
new(GamepadButtonInputId.RightShoulder, configuration.RightJoycon.ButtonR),
new(GamepadButtonInputId.RightTrigger, configuration.RightJoycon.ButtonZr),
new(GamepadButtonInputId.SingleRightTrigger1, configuration.RightJoycon.ButtonSr),
new(GamepadButtonInputId.SingleLeftTrigger1, configuration.RightJoycon.ButtonSl),
];
public static (short X, short Y) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigPhysicalKey> stickConfig)
{
short stickX = 0;
short stickY = 0;
if (snapshot.IsPressed(stickConfig.StickUp))
{
stickY += 1;
}
if (snapshot.IsPressed(stickConfig.StickDown))
{
stickY -= 1;
}
if (snapshot.IsPressed(stickConfig.StickRight))
{
stickX += 1;
}
if (snapshot.IsPressed(stickConfig.StickLeft))
{
stickX -= 1;
}
if (stickX == 0 && stickY == 0)
{
return (0, 0);
}
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
}
}

View File

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

View File

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

View File

@@ -1,10 +1,23 @@
using System;
using System.Runtime.InteropServices;
using UnicornEngine.Const;
namespace Ryujinx.Tests.Unicorn
{
public class UnicornAArch32 : IDisposable
{
struct UcArmCpReg
{
public uint Cp;
public uint Is64;
public uint Sec;
public uint CRn;
public uint CRm;
public uint Opc1;
public uint Opc2;
public uint Val;
}
internal readonly UnicornEngine.Unicorn Uc;
private bool _isDisposed;
@@ -38,7 +51,7 @@ namespace Ryujinx.Tests.Unicorn
public int Fpscr
{
get => (int)GetRegister(Arm.UC_ARM_REG_FPSCR) | ((int)GetRegister(Arm.UC_ARM_REG_FPSCR_NZCV));
get => (int)GetRegister(Arm.UC_ARM_REG_FPSCR);
set => SetRegister(Arm.UC_ARM_REG_FPSCR, (uint)value);
}
@@ -85,8 +98,22 @@ namespace Ryujinx.Tests.Unicorn
public UnicornAArch32()
{
Uc = new UnicornEngine.Unicorn(Common.UC_ARCH_ARM, Common.UC_MODE_LITTLE_ENDIAN);
SetRegister(Arm.UC_ARM_REG_C1_C0_2, GetRegister(Arm.UC_ARM_REG_C1_C0_2) | 0xf00000);
UcArmCpReg reg = new()
{
Cp = 15,
Is64 = 0,
Sec = 0,
CRn = 13,
Opc1 = 0,
CRm = 0,
Opc2 = 2
};
GetRegister(Arm.UC_ARM_REG_CP_REG, ref reg);
reg.Val |= 0xf00000;
SetRegister(Arm.UC_ARM_REG_CP_REG, reg);
SetRegister(Arm.UC_ARM_REG_FPEXC, 0x40000000);
}
@@ -204,6 +231,17 @@ namespace Ryujinx.Tests.Unicorn
SetVector(Arm.UC_ARM_REG_D0 + index * 2, value);
}
public void GetRegister<T>(int register, ref T obj) where T : unmanaged
{
Span<T> span = new(ref obj);
Span<byte> dataSpan = MemoryMarshal.Cast<T, byte>(span);
byte[] data = dataSpan.ToArray();
Uc.RegRead(register, data);
data.AsSpan().CopyTo(dataSpan);
}
public uint GetRegister(int register)
{
byte[] data = new byte[4];
@@ -213,6 +251,13 @@ namespace Ryujinx.Tests.Unicorn
return BitConverter.ToUInt32(data, 0);
}
public void SetRegister<T>(int register, T obj) where T : unmanaged
{
byte[] data = MemoryMarshal.Cast<T, byte>(new Span<T>(ref obj)).ToArray();
Uc.RegWrite(register, data);
}
public void SetRegister(int register, uint value)
{
byte[] data = BitConverter.GetBytes(value);

View File

@@ -7,6 +7,7 @@ using Ryujinx.Common.Memory;
using Ryujinx.Common.Memory.PartialUnmaps;
using Ryujinx.Cpu;
using Ryujinx.Cpu.Jit;
using Ryujinx.Cpu.Signal;
using Ryujinx.Memory;
using Ryujinx.Memory.Tracking;
using System;
@@ -60,6 +61,8 @@ namespace Ryujinx.Tests.Memory
new JitMemoryAllocator(),
new MockMemoryManager(),
AddressTable<ulong>.CreateForArm(true, MemoryManagerType.SoftwarePageTable));
NativeSignalHandler.InitializeSignalHandler();
}
[Test]

View File

@@ -7,8 +7,6 @@ using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.Cpu;
@@ -17,14 +15,12 @@ using Ryujinx.Graphics.OpenGL;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE;
using Ryujinx.Input;
using Ryujinx.Input.SDL3;
using Silk.NET.Vulkan;
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Headless
{
@@ -90,6 +86,19 @@ namespace Ryujinx.Headless
}
string gamepadName = gamepad.Name;
bool isNintendoStyle = false;
if (gamepad is SDL3Gamepad sdlGp)
{
// Nintendo vendor ID is 0x057E
isNintendoStyle = sdlGp.VendorId == 0x057E;
}
else
{
// Fallback to name-based detection
isNintendoStyle = gamepadName.Contains("Nintendo", StringComparison.OrdinalIgnoreCase);
}
gamepad.Dispose();
@@ -99,131 +108,21 @@ namespace Ryujinx.Headless
{
if (isKeyboard)
{
config = new StandardKeyboardInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.WindowKeyboard,
Id = null,
ControllerType = ControllerType.JoyconPair,
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 = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
null,
null,
ControllerType.JoyconPair,
index);
}
else
{
bool isNintendoStyle = gamepadName.Contains("Nintendo");
config = new StandardControllerInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL3,
Id = null,
ControllerType = ControllerType.JoyconPair,
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.SingleLeftTrigger0,
ButtonSr = ConfigGamepadInputId.SingleRightTrigger0,
},
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Left,
StickButton = ConfigGamepadInputId.LeftStick,
InvertStickX = false,
InvertStickY = false,
Rotate90CW = 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.SingleLeftTrigger1,
ButtonSr = ConfigGamepadInputId.SingleRightTrigger1,
},
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Right,
StickButton = ConfigGamepadInputId.RightStick,
InvertStickX = false,
InvertStickY = false,
Rotate90CW = false,
},
Motion = new StandardMotionConfigController
{
MotionBackend = MotionInputBackendType.GamepadDriver,
EnableMotion = true,
Sensitivity = 100,
GyroDeadzone = 1,
},
Rumble = new RumbleConfigController
{
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false,
UseHDRumble = true
},
};
config = InputConfigDefaults.CreateDefaultControllerConfiguration(
null,
null,
ControllerType.JoyconPair,
index,
isNintendoStyle);
}
}
else

View File

@@ -7,15 +7,15 @@ using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.Input
{
internal class AvaloniaKeyboard : IKeyboard
{
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private readonly AvaloniaKeyboardDriver _driver;
private readonly KeyboardInputMode _mode;
private StandardKeyboardInputConfig _configuration;
private readonly Lock _userMappingLock = new();
@@ -25,18 +25,12 @@ namespace Ryujinx.Ava.Input
public bool IsConnected => true;
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
private class ButtonMappingEntry(GamepadButtonInputId to, Key from)
{
public readonly GamepadButtonInputId To = to;
public readonly Key From = from;
}
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name)
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name, KeyboardInputMode mode)
{
_buttonsUserMapping = [];
_driver = driver;
_mode = mode;
Id = id;
Name = name;
}
@@ -58,22 +52,18 @@ namespace Ryujinx.Ava.Input
return result;
}
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
{
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
if (!entry.IsValid || result.IsPressed(entry.To))
{
continue;
}
// NOTE: Do not touch state of the button already pressed.
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -101,7 +91,7 @@ namespace Ryujinx.Ava.Input
{
try
{
return _driver.IsPressed(key);
return _driver.IsPressed(key, _mode);
}
catch
{
@@ -109,6 +99,19 @@ namespace Ryujinx.Ava.Input
}
}
public bool TryConsumePressedKey(out Key key)
{
try
{
return _driver.TryConsumePressedKey(_mode, out key);
}
catch
{
key = Key.Unknown;
return false;
}
}
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
@@ -117,53 +120,20 @@ namespace Ryujinx.Ava.Input
_buttonsUserMapping.Clear();
#pragma warning disable IDE0055 // Disable formatting
// Left JoyCon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
// Right JoyCon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
#pragma warning restore IDE0055
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
}
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
Logger.Debug?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
}
public void SetTriggerThreshold(float triggerThreshold)
{
// No operations
}
public bool HDRumble(VibrationValue left, VibrationValue right) => false;
public bool HDRumble(VibrationValue left, VibrationValue right)
{
return false;
}
public void SetTriggerThreshold(float triggerThreshold) { }
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
return false;
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) => false;
public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero;
@@ -174,41 +144,9 @@ namespace Ryujinx.Ava.Input
return value * ConvertRate;
}
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
{
short stickX = 0;
short stickY = 0;
if (snapshot.IsPressed((Key)stickConfig.StickUp))
{
stickY += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickDown))
{
stickY -= 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickRight))
{
stickX += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
{
stickX -= 1;
}
Vector2 stick = new(stickX, stickY);
stick = Vector2.Normalize(stick);
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
public void Clear()
{
_driver?.Clear();
_driver?.Clear(_mode);
}
public void Dispose() { }

View File

@@ -1,19 +1,55 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using AvaKey = Avalonia.Input.Key;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.Input
{
internal class AvaloniaKeyboardDriver : IGamepadDriver
internal class AvaloniaKeyboardDriver : IKeyboardModeDriver
{
private enum PhysicalKeySource
{
Direct,
ObservedFallback,
Unknown,
}
[Flags]
private enum CGEventFlags : ulong
{
AlphaShift = 1UL << 16 // CapsLock
}
private enum CGEventSourceStateID : uint
{
HIDSystemState = 1
}
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
private static extern CGEventFlags CGEventSourceFlagsState(CGEventSourceStateID stateID);
private static readonly string[] _keyboardIdentifers = ["0"];
private readonly Control _control;
private readonly HashSet<AvaKey> _pressedKeys;
private readonly Window _window;
private readonly HashSet<Key> _semanticPressedKeys;
private readonly HashSet<ConfigPhysicalKey> _physicalPressedKeys;
private readonly HashSet<Key> _keysToRestoreAfterActivation;
private readonly Dictionary<Key, ConfigPhysicalKey> _observedPhysicalKeysBySemanticKey;
private readonly Queue<Key> _semanticPressedKeyQueue;
private readonly Queue<Key> _physicalPressedKeyQueue;
private readonly Lock _pressedKeyQueueLock;
private readonly KeyboardInputMode _defaultMode;
public event EventHandler<KeyEventArgs> KeyPressed;
public event EventHandler<KeyEventArgs> KeyRelease;
@@ -22,14 +58,41 @@ namespace Ryujinx.Ava.Input
public string DriverName => "AvaloniaKeyboardDriver";
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
public AvaloniaKeyboardDriver(Control control)
public AvaloniaKeyboardDriver(Control control, KeyboardInputMode defaultMode = KeyboardInputMode.Semantic)
{
_control = control;
_pressedKeys = [];
_window = control as Window ?? TopLevel.GetTopLevel(control) as Window;
_semanticPressedKeys = [];
_physicalPressedKeys = [];
_keysToRestoreAfterActivation = [];
_observedPhysicalKeysBySemanticKey = [];
_semanticPressedKeyQueue = [];
_physicalPressedKeyQueue = [];
_pressedKeyQueueLock = new();
_defaultMode = defaultMode;
_control.KeyDown += OnKeyPress;
_control.KeyUp += OnKeyRelease;
_control.AddHandler(InputElement.KeyDownEvent, OnKeyPress, RoutingStrategies.Tunnel, true);
_control.AddHandler(InputElement.KeyUpEvent, OnKeyRelease, RoutingStrategies.Tunnel, true);
_control.TextInput += Control_TextInput;
_window?.Activated += Window_Activated;
_window?.Deactivated += Window_Deactivated;
}
private void Window_Activated(object sender, EventArgs e)
{
RestorePressedKeysAfterActivation();
}
private void Window_Deactivated(object sender, EventArgs e)
{
lock (_pressedKeyQueueLock)
{
_keysToRestoreAfterActivation.Clear();
_keysToRestoreAfterActivation.UnionWith(_semanticPressedKeys);
_observedPhysicalKeysBySemanticKey.Clear();
}
Clear();
}
private void Control_TextInput(object sender, TextInputEventArgs e)
@@ -50,13 +113,18 @@ namespace Ryujinx.Ava.Input
}
public IGamepad GetGamepad(string id)
{
return GetKeyboard(id, _defaultMode);
}
public IKeyboard GetKeyboard(string id, KeyboardInputMode mode)
{
if (!_keyboardIdentifers[0].Equals(id))
{
return null;
}
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]);
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.KeyboardLayout_KeyboardInputMode], mode);
}
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
@@ -65,40 +133,448 @@ namespace Ryujinx.Ava.Input
{
if (disposing)
{
_control.KeyUp -= OnKeyPress;
_control.KeyDown -= OnKeyRelease;
_control.RemoveHandler(InputElement.KeyDownEvent, OnKeyPress);
_control.RemoveHandler(InputElement.KeyUpEvent, OnKeyRelease);
_control.TextInput -= Control_TextInput;
if (_window != null)
{
_window.Activated -= Window_Activated;
_window.Deactivated -= Window_Deactivated;
}
_observedPhysicalKeysBySemanticKey.Clear();
}
}
protected void OnKeyPress(object sender, KeyEventArgs args)
{
_pressedKeys.Add(args.Key);
UpdateKeyStates(args, true);
KeyPressed?.Invoke(this, args);
}
protected void OnKeyRelease(object sender, KeyEventArgs args)
{
_pressedKeys.Remove(args.Key);
UpdateKeyStates(args, false);
KeyRelease?.Invoke(this, args);
}
internal bool IsPressed(Key key)
internal bool IsPressed(Key key, KeyboardInputMode mode)
{
if (key is Key.Unbound or Key.Unknown)
{
return false;
}
AvaloniaKeyboardMappingHelper.TryGetAvaKey(key, out AvaKey nativeKey);
if (key == Key.CapsLock)
{
return IsCapsLockOnMacOS();
}
return _pressedKeys.Contains(nativeKey);
return mode == KeyboardInputMode.Physical
? _physicalPressedKeys.Contains((ConfigPhysicalKey)(int)key)
: _semanticPressedKeys.Contains(key);
}
private bool IsCapsLockOnMacOS()
{
bool currentState = false;
try
{
if (OperatingSystem.IsMacOS())
{
CGEventFlags flags = CGEventSourceFlagsState(CGEventSourceStateID.HIDSystemState);
currentState = (flags & CGEventFlags.AlphaShift) != 0;
}
else
{
// Fallback: use Avalonia's tracked key state (semantic CapsLock)
if (AvaloniaKeyboardMappingHelper.TryGetAvaKey(Key.CapsLock, out AvaKey nativeKey))
{
currentState = _semanticPressedKeys.Contains(Key.CapsLock);
}
}
}
catch (Exception ex)
{
Logger.Debug?.Print(LogClass.UI, $"Failed to query CapsLock state: {ex}");
}
return currentState;
}
internal void Clear(KeyboardInputMode mode)
{
lock (_pressedKeyQueueLock)
{
if (mode == KeyboardInputMode.Physical)
{
_physicalPressedKeys.Clear();
_physicalPressedKeyQueue.Clear();
}
else
{
_semanticPressedKeys.Clear();
_semanticPressedKeyQueue.Clear();
}
}
}
public void Clear()
{
_pressedKeys.Clear();
lock (_pressedKeyQueueLock)
{
_semanticPressedKeys.Clear();
_physicalPressedKeys.Clear();
_semanticPressedKeyQueue.Clear();
_physicalPressedKeyQueue.Clear();
}
}
private void RestorePressedKeysAfterActivation()
{
if (!OperatingSystem.IsWindows())
{
lock (_pressedKeyQueueLock)
{
_keysToRestoreAfterActivation.Clear();
}
return;
}
lock (_pressedKeyQueueLock)
{
if (_keysToRestoreAfterActivation.Count == 0)
{
return;
}
foreach (Key key in _keysToRestoreAfterActivation)
{
if (!TryGetWindowsVirtualKey(key, out int virtualKey) ||
!IsWindowsKeyPressed(virtualKey))
{
continue;
}
_semanticPressedKeys.Add(key);
ConfigPhysicalKey physicalKey = GetPhysicalKeyForSemanticKey(key);
if (physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
{
_physicalPressedKeys.Add(physicalKey);
}
}
_keysToRestoreAfterActivation.Clear();
}
}
private ConfigPhysicalKey GetPhysicalKeyForSemanticKey(Key key)
{
if (_observedPhysicalKeysBySemanticKey.TryGetValue(key, out ConfigPhysicalKey physicalKey))
{
return physicalKey;
}
return key is >= Key.Unknown and < Key.Count
? (ConfigPhysicalKey)(int)key
: ConfigPhysicalKey.Unknown;
}
[SupportedOSPlatform("windows")]
private static bool IsWindowsKeyPressed(int virtualKey)
{
return (Win32NativeInterop.GetAsyncKeyState(virtualKey) & 0x8000) != 0;
}
private static bool TryGetWindowsVirtualKey(Key key, out int virtualKey)
{
switch (key)
{
case >= Key.A and <= Key.Z:
virtualKey = 'A' + (int)(key - Key.A);
return true;
case >= Key.Number0 and <= Key.Number9:
virtualKey = '0' + (int)(key - Key.Number0);
return true;
case >= Key.F1 and <= Key.F24:
virtualKey = 0x70 + (int)(key - Key.F1);
return true;
case Key.ShiftLeft:
virtualKey = 0xA0;
return true;
case Key.ShiftRight:
virtualKey = 0xA1;
return true;
case Key.ControlLeft:
virtualKey = 0xA2;
return true;
case Key.ControlRight:
virtualKey = 0xA3;
return true;
case Key.AltLeft:
virtualKey = 0xA4;
return true;
case Key.AltRight:
virtualKey = 0xA5;
return true;
case Key.WinLeft:
virtualKey = 0x5B;
return true;
case Key.WinRight:
virtualKey = 0x5C;
return true;
case Key.Menu:
virtualKey = 0x5D;
return true;
case Key.Up:
virtualKey = 0x26;
return true;
case Key.Down:
virtualKey = 0x28;
return true;
case Key.Left:
virtualKey = 0x25;
return true;
case Key.Right:
virtualKey = 0x27;
return true;
case Key.Enter:
virtualKey = 0x0D;
return true;
case Key.Escape:
virtualKey = 0x1B;
return true;
case Key.Space:
virtualKey = 0x20;
return true;
case Key.Tab:
virtualKey = 0x09;
return true;
case Key.BackSpace:
virtualKey = 0x08;
return true;
case Key.Insert:
virtualKey = 0x2D;
return true;
case Key.Delete:
virtualKey = 0x2E;
return true;
case Key.PageUp:
virtualKey = 0x21;
return true;
case Key.PageDown:
virtualKey = 0x22;
return true;
case Key.Home:
virtualKey = 0x24;
return true;
case Key.End:
virtualKey = 0x23;
return true;
case Key.CapsLock:
virtualKey = 0x14;
return true;
case Key.ScrollLock:
virtualKey = 0x91;
return true;
case Key.PrintScreen:
virtualKey = 0x2C;
return true;
case Key.Pause:
virtualKey = 0x13;
return true;
case Key.NumLock:
virtualKey = 0x90;
return true;
case Key.Clear:
virtualKey = 0x0C;
return true;
case >= Key.Keypad0 and <= Key.Keypad9:
virtualKey = 0x60 + (int)(key - Key.Keypad0);
return true;
case Key.KeypadDivide:
virtualKey = 0x6F;
return true;
case Key.KeypadMultiply:
virtualKey = 0x6A;
return true;
case Key.KeypadSubtract:
virtualKey = 0x6D;
return true;
case Key.KeypadAdd:
virtualKey = 0x6B;
return true;
case Key.KeypadDecimal:
virtualKey = 0x6E;
return true;
case Key.KeypadEnter:
virtualKey = 0x0D;
return true;
case Key.Tilde:
virtualKey = 0xC0;
return true;
case Key.Grave:
virtualKey = 0xE2;
return true;
case Key.Minus:
virtualKey = 0xBD;
return true;
case Key.Plus:
virtualKey = 0xBB;
return true;
case Key.BracketLeft:
virtualKey = 0xDB;
return true;
case Key.BracketRight:
virtualKey = 0xDD;
return true;
case Key.Semicolon:
virtualKey = 0xBA;
return true;
case Key.Quote:
virtualKey = 0xDE;
return true;
case Key.Comma:
virtualKey = 0xBC;
return true;
case Key.Period:
virtualKey = 0xBE;
return true;
case Key.Slash:
virtualKey = 0xBF;
return true;
case Key.BackSlash:
virtualKey = 0xDC;
return true;
default:
virtualKey = 0;
return false;
}
}
internal bool TryConsumePressedKey(KeyboardInputMode mode, out Key key)
{
lock (_pressedKeyQueueLock)
{
Queue<Key> queue = mode == KeyboardInputMode.Physical ? _physicalPressedKeyQueue : _semanticPressedKeyQueue;
if (queue.TryDequeue(out key))
{
return true;
}
}
key = Key.Unknown;
return false;
}
private static void UpdateKeyState(HashSet<Key> pressedKeys, Key key, bool isPressed)
{
if (key is Key.Unknown or Key.Unbound)
{
return;
}
if (isPressed)
{
pressedKeys.Add(key);
return;
}
pressedKeys.Remove(key);
}
private static void UpdateKeyState(HashSet<ConfigPhysicalKey> pressedKeys, ConfigPhysicalKey key, bool isPressed)
{
if (key is ConfigPhysicalKey.Unknown or ConfigPhysicalKey.Unbound)
{
return;
}
if (isPressed)
{
pressedKeys.Add(key);
return;
}
pressedKeys.Remove(key);
}
private void UpdateKeyStates(KeyEventArgs args, bool isPressed)
{
Key semanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.Key);
Key resolvedSemanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey, args.Key);
ConfigPhysicalKey physicalKey = GetPhysicalInputKey(args, semanticKey, out PhysicalKeySource physicalKeySource);
bool semanticWasPressed = _semanticPressedKeys.Contains(resolvedSemanticKey);
bool physicalWasPressed = _physicalPressedKeys.Contains(physicalKey);
bool semanticStateChanged = resolvedSemanticKey is not Key.Unknown and not Key.Unbound && semanticWasPressed != isPressed;
bool physicalStateChanged = physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound && physicalWasPressed != isPressed;
bool bufferedSemanticPress = false;
bool bufferedPhysicalPress = false;
UpdateKeyState(_semanticPressedKeys, resolvedSemanticKey, isPressed);
UpdateKeyState(_physicalPressedKeys, physicalKey, isPressed);
if (isPressed)
{
lock (_pressedKeyQueueLock)
{
if (!semanticWasPressed && resolvedSemanticKey is not Key.Unknown and not Key.Unbound)
{
_semanticPressedKeyQueue.Enqueue(resolvedSemanticKey);
bufferedSemanticPress = true;
}
if (!physicalWasPressed && physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
{
_physicalPressedKeyQueue.Enqueue((Key)(int)physicalKey);
bufferedPhysicalPress = true;
}
}
}
if (isPressed &&
semanticKey is not Key.Unknown and not Key.Unbound &&
physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
{
_observedPhysicalKeysBySemanticKey[semanticKey] = physicalKey;
}
if (ConfigurationState.Instance.Logger.EnableAvaloniaLog &&
(semanticStateChanged || physicalStateChanged))
{
Logger.Info?.Print(
LogClass.UI,
$"Keyboard {(isPressed ? "down" : "up")}: avaloniaKey={args.Key}, avaloniaPhysical={args.PhysicalKey}, keySymbol={FormatKeySymbol(args.KeySymbol)}, modifiers={args.KeyModifiers}, semantic={semanticKey}, resolvedSemantic={resolvedSemanticKey}, physical={physicalKey}, physicalSource={physicalKeySource}, bufferedSemantic={bufferedSemanticPress}, bufferedPhysical={bufferedPhysicalPress}, semanticPressed={_semanticPressedKeys.Count}, physicalPressed={_physicalPressedKeys.Count}");
}
}
private ConfigPhysicalKey GetPhysicalInputKey(KeyEventArgs args, Key semanticKey, out PhysicalKeySource source)
{
Key key = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey);
if (key is >= Key.Unknown and < Key.Count)
{
source = PhysicalKeySource.Direct;
return (ConfigPhysicalKey)(int)key;
}
if (semanticKey is not Key.Unknown and not Key.Unbound &&
_observedPhysicalKeysBySemanticKey.TryGetValue(semanticKey, out ConfigPhysicalKey observedPhysicalKey))
{
source = PhysicalKeySource.ObservedFallback;
return observedPhysicalKey;
}
source = PhysicalKeySource.Unknown;
return ConfigPhysicalKey.Unknown;
}
private static string FormatKeySymbol(string keySymbol)
{
return string.IsNullOrEmpty(keySymbol) ? "<none>" : keySymbol;
}
public void Dispose()

View File

@@ -2,6 +2,7 @@ using Ryujinx.Input;
using System;
using System.Collections.Generic;
using AvaKey = Avalonia.Input.Key;
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
namespace Ryujinx.Ava.Input
{
@@ -132,7 +133,8 @@ namespace Ryujinx.Ava.Input
AvaKey.D8,
AvaKey.D9,
AvaKey.OemTilde,
AvaKey.OemTilde,AvaKey.OemMinus,
AvaKey.Oem102,
AvaKey.OemMinus,
AvaKey.OemPlus,
AvaKey.OemOpenBrackets,
AvaKey.OemCloseBrackets,
@@ -147,7 +149,149 @@ namespace Ryujinx.Ava.Input
AvaKey.None
];
private static readonly AvaPhysicalKey[] _physicalKeyMapping =
[
// NOTE: Invalid
AvaPhysicalKey.None,
AvaPhysicalKey.ShiftLeft,
AvaPhysicalKey.ShiftRight,
AvaPhysicalKey.ControlLeft,
AvaPhysicalKey.ControlRight,
AvaPhysicalKey.AltLeft,
AvaPhysicalKey.AltRight,
AvaPhysicalKey.MetaLeft,
AvaPhysicalKey.MetaRight,
AvaPhysicalKey.ContextMenu,
AvaPhysicalKey.F1,
AvaPhysicalKey.F2,
AvaPhysicalKey.F3,
AvaPhysicalKey.F4,
AvaPhysicalKey.F5,
AvaPhysicalKey.F6,
AvaPhysicalKey.F7,
AvaPhysicalKey.F8,
AvaPhysicalKey.F9,
AvaPhysicalKey.F10,
AvaPhysicalKey.F11,
AvaPhysicalKey.F12,
AvaPhysicalKey.F13,
AvaPhysicalKey.F14,
AvaPhysicalKey.F15,
AvaPhysicalKey.F16,
AvaPhysicalKey.F17,
AvaPhysicalKey.F18,
AvaPhysicalKey.F19,
AvaPhysicalKey.F20,
AvaPhysicalKey.F21,
AvaPhysicalKey.F22,
AvaPhysicalKey.F23,
AvaPhysicalKey.F24,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.ArrowUp,
AvaPhysicalKey.ArrowDown,
AvaPhysicalKey.ArrowLeft,
AvaPhysicalKey.ArrowRight,
AvaPhysicalKey.Enter,
AvaPhysicalKey.Escape,
AvaPhysicalKey.Space,
AvaPhysicalKey.Tab,
AvaPhysicalKey.Backspace,
AvaPhysicalKey.Insert,
AvaPhysicalKey.Delete,
AvaPhysicalKey.PageUp,
AvaPhysicalKey.PageDown,
AvaPhysicalKey.Home,
AvaPhysicalKey.End,
AvaPhysicalKey.CapsLock,
AvaPhysicalKey.ScrollLock,
AvaPhysicalKey.PrintScreen,
AvaPhysicalKey.Pause,
AvaPhysicalKey.NumLock,
AvaPhysicalKey.NumPadClear,
AvaPhysicalKey.NumPad0,
AvaPhysicalKey.NumPad1,
AvaPhysicalKey.NumPad2,
AvaPhysicalKey.NumPad3,
AvaPhysicalKey.NumPad4,
AvaPhysicalKey.NumPad5,
AvaPhysicalKey.NumPad6,
AvaPhysicalKey.NumPad7,
AvaPhysicalKey.NumPad8,
AvaPhysicalKey.NumPad9,
AvaPhysicalKey.NumPadDivide,
AvaPhysicalKey.NumPadMultiply,
AvaPhysicalKey.NumPadSubtract,
AvaPhysicalKey.NumPadAdd,
AvaPhysicalKey.NumPadDecimal,
AvaPhysicalKey.NumPadEnter,
AvaPhysicalKey.A,
AvaPhysicalKey.B,
AvaPhysicalKey.C,
AvaPhysicalKey.D,
AvaPhysicalKey.E,
AvaPhysicalKey.F,
AvaPhysicalKey.G,
AvaPhysicalKey.H,
AvaPhysicalKey.I,
AvaPhysicalKey.J,
AvaPhysicalKey.K,
AvaPhysicalKey.L,
AvaPhysicalKey.M,
AvaPhysicalKey.N,
AvaPhysicalKey.O,
AvaPhysicalKey.P,
AvaPhysicalKey.Q,
AvaPhysicalKey.R,
AvaPhysicalKey.S,
AvaPhysicalKey.T,
AvaPhysicalKey.U,
AvaPhysicalKey.V,
AvaPhysicalKey.W,
AvaPhysicalKey.X,
AvaPhysicalKey.Y,
AvaPhysicalKey.Z,
AvaPhysicalKey.Digit0,
AvaPhysicalKey.Digit1,
AvaPhysicalKey.Digit2,
AvaPhysicalKey.Digit3,
AvaPhysicalKey.Digit4,
AvaPhysicalKey.Digit5,
AvaPhysicalKey.Digit6,
AvaPhysicalKey.Digit7,
AvaPhysicalKey.Digit8,
AvaPhysicalKey.Digit9,
AvaPhysicalKey.Backquote,
AvaPhysicalKey.IntlBackslash,
AvaPhysicalKey.Minus,
AvaPhysicalKey.Equal,
AvaPhysicalKey.BracketLeft,
AvaPhysicalKey.BracketRight,
AvaPhysicalKey.Semicolon,
AvaPhysicalKey.Quote,
AvaPhysicalKey.Comma,
AvaPhysicalKey.Period,
AvaPhysicalKey.Slash,
AvaPhysicalKey.Backslash,
// NOTE: invalid
AvaPhysicalKey.None
];
private static readonly Dictionary<AvaKey, Key> _avaKeyMapping;
private static readonly Dictionary<AvaPhysicalKey, Key> _avaPhysicalKeyMapping;
static AvaloniaKeyboardMappingHelper()
{
@@ -155,21 +299,42 @@ namespace Ryujinx.Ava.Input
// NOTE: Avalonia.Input.Key is not contiguous and quite large, so use a dictionary instead of an array.
_avaKeyMapping = new Dictionary<AvaKey, Key>();
_avaPhysicalKeyMapping = new Dictionary<AvaPhysicalKey, Key>();
foreach (Key key in inputKeys)
{
if (TryGetAvaKey(key, out AvaKey index))
if (TryGetAvaKey(key, out AvaKey avaKey))
{
_avaKeyMapping[index] = key;
_avaKeyMapping[avaKey] = key;
}
if (TryGetAvaPhysicalKey(key, out AvaPhysicalKey avaPhysicalKey))
{
_avaPhysicalKeyMapping[avaPhysicalKey] = key;
}
}
// Alias additional Avalonia key values to improve non-US layout support.
_avaKeyMapping[AvaKey.Oem1] = Key.Semicolon;
_avaKeyMapping[AvaKey.Oem2] = Key.Slash;
_avaKeyMapping[AvaKey.Oem3] = Key.Tilde;
_avaKeyMapping[AvaKey.Oem4] = Key.BracketLeft;
_avaKeyMapping[AvaKey.Oem5] = Key.BackSlash;
_avaKeyMapping[AvaKey.Oem6] = Key.BracketRight;
_avaKeyMapping[AvaKey.Oem7] = Key.Quote;
_avaKeyMapping[AvaKey.OemBackslash] = Key.Grave;
_avaKeyMapping[AvaKey.Oem102] = Key.Grave;
// Common alternates for non-US/JIS physical keys.
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlRo] = Key.BackSlash;
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlYen] = Key.BackSlash;
}
public static bool TryGetAvaKey(Key key, out AvaKey avaKey)
{
avaKey = AvaKey.None;
bool keyExist = (int)key < _keyMapping.Length;
bool keyExist = key < Key.Count && (int)key < _keyMapping.Length;
if (keyExist)
{
avaKey = _keyMapping[(int)key];
@@ -178,9 +343,34 @@ namespace Ryujinx.Ava.Input
return keyExist;
}
public static bool TryGetAvaPhysicalKey(Key key, out AvaPhysicalKey avaPhysicalKey)
{
avaPhysicalKey = AvaPhysicalKey.None;
bool keyExist = key < Key.Count && (int)key < _physicalKeyMapping.Length;
if (keyExist)
{
avaPhysicalKey = _physicalKeyMapping[(int)key];
}
return keyExist;
}
public static Key ToInputKey(AvaKey key)
{
return _avaKeyMapping.GetValueOrDefault(key, Key.Unknown);
}
public static Key ToInputKey(AvaPhysicalKey key)
{
return _avaPhysicalKeyMapping.GetValueOrDefault(key, Key.Unknown);
}
public static Key ToInputKey(AvaPhysicalKey physicalKey, AvaKey key)
{
Key inputKey = ToInputKey(key);
return inputKey != Key.Unknown ? inputKey : ToInputKey(physicalKey);
}
}
}

View File

@@ -818,8 +818,7 @@ namespace Ryujinx.Ava.Systems
if (!Device.LoadCart(ApplicationPath, romFsFiles[0]))
{
await ContentDialogHelper.CreateErrorDialog(
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.Error_NoUnpackedApplicationFoundInFolder));
Device.Dispose();
cts.Cancel();
@@ -831,8 +830,7 @@ namespace Ryujinx.Ava.Systems
Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
if (!Device.LoadCart(ApplicationPath))
{
await ContentDialogHelper.CreateErrorDialog(
"Please specify an unpacked game directory with a valid exefs or NSO/NRO.");
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.Error_NoUnpackedApplicationFoundInFolder));
Device.Dispose();
cts.Cancel();
throw new OperationCanceledException(cts.Token);
@@ -1094,10 +1092,10 @@ namespace Ryujinx.Ava.Systems
{
Dispatcher.UIThread.InvokeAsync(() =>
{
if (_viewModel.StartGamesInFullscreen)
if (_viewModel.StartGamesInFullscreen && _viewModel.WindowState is not WindowState.FullScreen)
{
_viewModel.WindowState = WindowState.FullScreen;
_viewModel.Window.TitleBar.ExtendsContentIntoTitleBar = true;
// Use the view model toggle so decoration ordering matches user toggles.
_viewModel.ToggleFullscreen();
}
if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUi)
@@ -1304,6 +1302,11 @@ namespace Ryujinx.Ava.Systems
return false;
}
if (!_viewModel.IsActive)
{
_inputManager.KeyboardDriver.Clear();
}
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (_viewModel.IsActive)

View File

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

View File

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

View File

@@ -46,7 +46,7 @@ namespace Ryujinx.Ava.Systems
LargeImageText = TruncateToByteLength(_description)
},
Details = "Main Menu",
State = "Idling",
State = "Waiting",
Timestamps = EmulatorStartedAt
};

View File

@@ -1069,7 +1069,6 @@ namespace Ryujinx.Ava.Systems.PlayReport
_ => FormattedValue.ForceReset
};
private static FormattedValue TomodachiLifeLTD_Status(SingleValue value)
{
MessagePackObject messagePackObject = value.Matched.PackedValue;
@@ -1077,8 +1076,9 @@ namespace Ryujinx.Ava.Systems.PlayReport
int miiCount = messagePackObjectDictionary["MiiNum"].AsInt32();
int fountainLevel = messagePackObjectDictionary["FountainLevel"].AsInt32();
return $"Looking after {"Mii".ToQuantity(miiCount)}, with an island level of {fountainLevel}";
// Fountain Level should be kept consistent throughout code, so I basically made sure of it
return $"Looking after {"Mii".ToQuantity(miiCount)}, with a fountain level of {fountainLevel}";
}
private static FormattedValue AnimalCrossingNewHorizons_AppCommon(SingleValue value)
@@ -1088,5 +1088,33 @@ namespace Ryujinx.Ava.Systems.PlayReport
return $"Living on {messagePackObjectDictionary["LandName"].AsString()} Island";
}
private static FormattedValue MiitopiaRPC(SparseMultiValue values)
{
if (values.Matched.TryGetValue("gold", out Value gold) && values.Matched.TryGetValue("stage", out Value location))
{
return $"{LocFinal(location.ToString())} with {gold} gold";
}
if (values.Matched.TryGetValue("secret", out Value secret)) // Yes "secret" is unused, but it only appears in the MII selector.
{
return $"In the MII selector";
}
return $"At the main menu";
static string LocFinal(string? location) => location switch
{
"0" => "Somewhere in Miitopia",
"1" => "Wandering around Greenhorne",
"2" => "Trodding through Neksdor",
"3" => "Exploring The Realm of the Fey",
"4" => "Burning their feet at Karkaton",
"5" => "Soaring in the skies of Miitopia",
"6" => "Fighting up The Sky Scraper",
"7" => "Traveling Miitopia",
_ => "Wandering"
};
}
}
}

View File

@@ -132,6 +132,12 @@ namespace Ryujinx.Ava.Systems.PlayReport
.WithDescription("based on your island name.")
.AddValueFormatter("AppCmn", AnimalCrossingNewHorizons_AppCommon)
)
.AddSpec(
"01003da010e8a000", // Miitopia 01003da010e8a000
spec => spec
.WithDescription("based on gold count, report info only in the mii selector, and gamestage (progression)")
.AddSparseMultiValueFormatter(["gold", "secret", "stage"], MiitopiaRPC)
)
);
private static string Playing(string game) => $"Playing {game}";

View File

@@ -15,6 +15,7 @@ namespace Ryujinx.Ava.UI.Applet
class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler
{
private MainWindow _parent;
private AvaloniaKeyboardDriver _avaloniaKeyboardDriver;
private readonly OffscreenTextBox _hiddenTextBox;
private bool _canProcessInput;
private IDisposable _textChangedSubscription;
@@ -27,6 +28,7 @@ namespace Ryujinx.Ava.UI.Applet
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver)
{
_avaloniaKeyboardDriver = avaloniaKeyboardDriver;
avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed;
avaloniaKeyboardDriver.KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease;
avaloniaKeyboardDriver.TextInput += AvaloniaDynamicTextInputHandler_TextInput;
@@ -65,7 +67,7 @@ namespace Ryujinx.Ava.UI.Applet
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e)
{
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
{
@@ -85,7 +87,7 @@ namespace Ryujinx.Ava.UI.Applet
private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e)
{
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
{
@@ -115,11 +117,11 @@ namespace Ryujinx.Ava.UI.Applet
public void Dispose()
{
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver)
if (_avaloniaKeyboardDriver != null)
{
avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
_avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
_avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
_avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
}
_textChangedSubscription?.Dispose();

View File

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

View File

@@ -5,6 +5,8 @@ using Ryujinx.Common.Configuration.Hid.Controller;
using System;
using System.Collections.Generic;
using System.Globalization;
using Key = Ryujinx.Input.Key;
using HidKey = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Ava.UI.Helpers
{
@@ -12,79 +14,6 @@ namespace Ryujinx.Ava.UI.Helpers
{
public static readonly KeyValueConverter Instance = new();
private static readonly Dictionary<Key, LocaleKeys> _keysMap = new()
{
{ Key.Unknown, LocaleKeys.KeyUnknown },
{ Key.ShiftLeft, LocaleKeys.KeyShiftLeft },
{ Key.ShiftRight, LocaleKeys.KeyShiftRight },
{ Key.ControlLeft, LocaleKeys.KeyControlLeft },
{ Key.ControlRight, LocaleKeys.KeyControlRight },
{ Key.AltLeft, LocaleKeys.KeyAltLeft },
{ Key.AltRight, LocaleKeys.KeyAltRight },
{ Key.WinLeft, LocaleKeys.KeyWinLeft },
{ Key.WinRight, LocaleKeys.KeyWinRight },
{ Key.Up, LocaleKeys.KeyUp },
{ Key.Down, LocaleKeys.KeyDown },
{ Key.Left, LocaleKeys.KeyLeft },
{ Key.Right, LocaleKeys.KeyRight },
{ Key.Enter, LocaleKeys.KeyEnter },
{ Key.Escape, LocaleKeys.KeyEscape },
{ Key.Space, LocaleKeys.KeySpace },
{ Key.Tab, LocaleKeys.KeyTab },
{ Key.BackSpace, LocaleKeys.KeyBackSpace },
{ Key.Insert, LocaleKeys.KeyInsert },
{ Key.Delete, LocaleKeys.KeyDelete },
{ Key.PageUp, LocaleKeys.KeyPageUp },
{ Key.PageDown, LocaleKeys.KeyPageDown },
{ Key.Home, LocaleKeys.KeyHome },
{ Key.End, LocaleKeys.KeyEnd },
{ Key.CapsLock, LocaleKeys.KeyCapsLock },
{ Key.ScrollLock, LocaleKeys.KeyScrollLock },
{ Key.PrintScreen, LocaleKeys.KeyPrintScreen },
{ Key.Pause, LocaleKeys.KeyPause },
{ Key.NumLock, LocaleKeys.KeyNumLock },
{ Key.Clear, LocaleKeys.KeyClear },
{ Key.Keypad0, LocaleKeys.KeyKeypad0 },
{ Key.Keypad1, LocaleKeys.KeyKeypad1 },
{ Key.Keypad2, LocaleKeys.KeyKeypad2 },
{ Key.Keypad3, LocaleKeys.KeyKeypad3 },
{ Key.Keypad4, LocaleKeys.KeyKeypad4 },
{ Key.Keypad5, LocaleKeys.KeyKeypad5 },
{ Key.Keypad6, LocaleKeys.KeyKeypad6 },
{ Key.Keypad7, LocaleKeys.KeyKeypad7 },
{ Key.Keypad8, LocaleKeys.KeyKeypad8 },
{ Key.Keypad9, LocaleKeys.KeyKeypad9 },
{ Key.KeypadDivide, LocaleKeys.KeyKeypadDivide },
{ Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply },
{ Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract },
{ Key.KeypadAdd, LocaleKeys.KeyKeypadAdd },
{ Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal },
{ Key.KeypadEnter, LocaleKeys.KeyKeypadEnter },
{ Key.Number0, LocaleKeys.KeyNumber0 },
{ Key.Number1, LocaleKeys.KeyNumber1 },
{ Key.Number2, LocaleKeys.KeyNumber2 },
{ Key.Number3, LocaleKeys.KeyNumber3 },
{ Key.Number4, LocaleKeys.KeyNumber4 },
{ Key.Number5, LocaleKeys.KeyNumber5 },
{ Key.Number6, LocaleKeys.KeyNumber6 },
{ Key.Number7, LocaleKeys.KeyNumber7 },
{ Key.Number8, LocaleKeys.KeyNumber8 },
{ Key.Number9, LocaleKeys.KeyNumber9 },
{ Key.Tilde, LocaleKeys.KeyTilde },
{ Key.Grave, LocaleKeys.KeyGrave },
{ Key.Minus, LocaleKeys.KeyMinus },
{ Key.Plus, LocaleKeys.KeyPlus },
{ Key.BracketLeft, LocaleKeys.KeyBracketLeft },
{ Key.BracketRight, LocaleKeys.KeyBracketRight },
{ Key.Semicolon, LocaleKeys.KeySemicolon },
{ Key.Quote, LocaleKeys.KeyQuote },
{ Key.Comma, LocaleKeys.KeyComma },
{ Key.Period, LocaleKeys.KeyPeriod },
{ Key.Slash, LocaleKeys.KeySlash },
{ Key.BackSlash, LocaleKeys.KeyBackSlash },
{ Key.Unbound, LocaleKeys.KeyUnbound },
};
private static readonly Dictionary<GamepadInputId, LocaleKeys> _gamepadInputIdMap = new()
{
{ GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick },
@@ -110,78 +39,43 @@ namespace Ryujinx.Ava.UI.Helpers
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
{ GamepadInputId.Unbound, LocaleKeys.KeyUnbound},
{ GamepadInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
};
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
{
{ StickInputId.Left, LocaleKeys.StickLeft},
{ StickInputId.Right, LocaleKeys.StickRight},
{ StickInputId.Unbound, LocaleKeys.KeyUnbound},
{ StickInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
};
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string keyString = string.Empty;
LocaleKeys localeKey;
switch (value)
return value switch
{
case Key key:
if (_keysMap.TryGetValue(key, out localeKey))
{
if (OperatingSystem.IsMacOS())
{
localeKey = localeKey switch
{
LocaleKeys.KeyControlLeft => LocaleKeys.KeyMacControlLeft,
LocaleKeys.KeyControlRight => LocaleKeys.KeyMacControlRight,
LocaleKeys.KeyAltLeft => LocaleKeys.KeyMacAltLeft,
LocaleKeys.KeyAltRight => LocaleKeys.KeyMacAltRight,
LocaleKeys.KeyWinLeft => LocaleKeys.KeyMacWinLeft,
LocaleKeys.KeyWinRight => LocaleKeys.KeyMacWinRight,
_ => localeKey
};
}
keyString = LocaleManager.Instance[localeKey];
}
else
{
keyString = key.ToString();
}
break;
case GamepadInputId gamepadInputId:
if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey))
{
keyString = LocaleManager.Instance[localeKey];
}
else
{
keyString = gamepadInputId.ToString();
}
break;
case StickInputId stickInputId:
if (_stickInputIdMap.TryGetValue(stickInputId, out localeKey))
{
keyString = LocaleManager.Instance[localeKey];
}
else
{
keyString = stickInputId.ToString();
}
break;
}
return keyString;
Key key => KeyboardLayoutLocaleHelper.TryGetSemanticLabel(key, out string localizedKeyLabel)
? localizedKeyLabel
: key.ToString(),
HidKey key => KeyboardLayoutLocaleHelper.TryGetSemanticLabel((Key)(int)key, out string localizedHidKeyLabel)
? localizedHidKeyLabel
: key.ToString(),
PhysicalKey physicalKey => PhysicalKeyLabelHelper.GetDisplayString(physicalKey),
GamepadInputId gamepadInputId => GetLocalizedMappedValue(gamepadInputId, _gamepadInputIdMap),
StickInputId stickInputId => GetLocalizedMappedValue(stickInputId, _stickInputIdMap),
_ => string.Empty,
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
private static string GetLocalizedMappedValue<T>(T value, IReadOnlyDictionary<T, LocaleKeys> map) where T : notnull
{
return map.TryGetValue(value, out LocaleKeys localeKey)
? LocaleManager.Instance[localeKey]
: value.ToString();
}
}
}

View File

@@ -0,0 +1,28 @@
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.UI.Models;
using System;
using System.Globalization;
namespace Ryujinx.Ava.UI.Helpers
{
internal class InputDeviceNameConverter : MarkupExtension, IValueConverter
{
public static readonly InputDeviceNameConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is ValueTuple<DeviceType, string, string> device ? device.Item3 : string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return Instance;
}
}
}

View File

@@ -0,0 +1,142 @@
using Ryujinx.Ava.Common.Locale;
using System;
using System.Collections.Generic;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using InputKey = Ryujinx.Input.Key;
namespace Ryujinx.Ava.UI.Helpers
{
internal static class KeyboardLayoutLocaleHelper
{
private static readonly Dictionary<InputKey, LocaleKeys> _sharedLocalizedKeysMap = new()
{
[InputKey.Unknown] = LocaleKeys.KeyboardLayout_KeyUnknown,
[InputKey.ShiftLeft] = LocaleKeys.KeyboardLayout_KeyShiftLeft,
[InputKey.ShiftRight] = LocaleKeys.KeyboardLayout_KeyShiftRight,
[InputKey.ControlLeft] = LocaleKeys.KeyboardLayout_KeyControlLeft,
[InputKey.ControlRight] = LocaleKeys.KeyboardLayout_KeyControlRight,
[InputKey.AltLeft] = LocaleKeys.KeyboardLayout_KeyAltLeft,
[InputKey.AltRight] = LocaleKeys.KeyboardLayout_KeyAltRight,
[InputKey.WinLeft] = LocaleKeys.KeyboardLayout_KeyWinLeft,
[InputKey.WinRight] = LocaleKeys.KeyboardLayout_KeyWinRight,
[InputKey.Up] = LocaleKeys.KeyboardLayout_KeyUp,
[InputKey.Down] = LocaleKeys.KeyboardLayout_KeyDown,
[InputKey.Left] = LocaleKeys.KeyboardLayout_KeyLeft,
[InputKey.Right] = LocaleKeys.KeyboardLayout_KeyRight,
[InputKey.Enter] = LocaleKeys.KeyboardLayout_KeyEnter,
[InputKey.Escape] = LocaleKeys.KeyboardLayout_KeyEscape,
[InputKey.Space] = LocaleKeys.KeyboardLayout_KeySpace,
[InputKey.Tab] = LocaleKeys.KeyboardLayout_KeyTab,
[InputKey.BackSpace] = LocaleKeys.KeyboardLayout_KeyBackSpace,
[InputKey.Insert] = LocaleKeys.KeyboardLayout_KeyInsert,
[InputKey.Delete] = LocaleKeys.KeyboardLayout_KeyDelete,
[InputKey.PageUp] = LocaleKeys.KeyboardLayout_KeyPageUp,
[InputKey.PageDown] = LocaleKeys.KeyboardLayout_KeyPageDown,
[InputKey.Home] = LocaleKeys.KeyboardLayout_KeyHome,
[InputKey.End] = LocaleKeys.KeyboardLayout_KeyEnd,
[InputKey.CapsLock] = LocaleKeys.KeyboardLayout_KeyCapsLock,
[InputKey.ScrollLock] = LocaleKeys.KeyboardLayout_KeyScrollLock,
[InputKey.PrintScreen] = LocaleKeys.KeyboardLayout_KeyPrintScreen,
[InputKey.Pause] = LocaleKeys.KeyboardLayout_KeyPause,
[InputKey.NumLock] = LocaleKeys.KeyboardLayout_KeyNumLock,
[InputKey.Clear] = LocaleKeys.KeyboardLayout_KeyClear,
[InputKey.Keypad0] = LocaleKeys.KeyboardLayout_KeyKeypad0,
[InputKey.Keypad1] = LocaleKeys.KeyboardLayout_KeyKeypad1,
[InputKey.Keypad2] = LocaleKeys.KeyboardLayout_KeyKeypad2,
[InputKey.Keypad3] = LocaleKeys.KeyboardLayout_KeyKeypad3,
[InputKey.Keypad4] = LocaleKeys.KeyboardLayout_KeyKeypad4,
[InputKey.Keypad5] = LocaleKeys.KeyboardLayout_KeyKeypad5,
[InputKey.Keypad6] = LocaleKeys.KeyboardLayout_KeyKeypad6,
[InputKey.Keypad7] = LocaleKeys.KeyboardLayout_KeyKeypad7,
[InputKey.Keypad8] = LocaleKeys.KeyboardLayout_KeyKeypad8,
[InputKey.Keypad9] = LocaleKeys.KeyboardLayout_KeyKeypad9,
[InputKey.KeypadDivide] = LocaleKeys.KeyboardLayout_KeyKeypadDivide,
[InputKey.KeypadMultiply] = LocaleKeys.KeyboardLayout_KeyKeypadMultiply,
[InputKey.KeypadSubtract] = LocaleKeys.KeyboardLayout_KeyKeypadSubtract,
[InputKey.KeypadAdd] = LocaleKeys.KeyboardLayout_KeyKeypadAdd,
[InputKey.KeypadDecimal] = LocaleKeys.KeyboardLayout_KeyKeypadDecimal,
[InputKey.KeypadEnter] = LocaleKeys.KeyboardLayout_KeyKeypadEnter,
[InputKey.Unbound] = LocaleKeys.KeyboardLayout_KeyUnbound,
};
private static readonly Dictionary<InputKey, LocaleKeys> _semanticPrintableKeysMap = new()
{
[InputKey.Number0] = LocaleKeys.KeyboardLayout_KeyNumber0,
[InputKey.Number1] = LocaleKeys.KeyboardLayout_KeyNumber1,
[InputKey.Number2] = LocaleKeys.KeyboardLayout_KeyNumber2,
[InputKey.Number3] = LocaleKeys.KeyboardLayout_KeyNumber3,
[InputKey.Number4] = LocaleKeys.KeyboardLayout_KeyNumber4,
[InputKey.Number5] = LocaleKeys.KeyboardLayout_KeyNumber5,
[InputKey.Number6] = LocaleKeys.KeyboardLayout_KeyNumber6,
[InputKey.Number7] = LocaleKeys.KeyboardLayout_KeyNumber7,
[InputKey.Number8] = LocaleKeys.KeyboardLayout_KeyNumber8,
[InputKey.Number9] = LocaleKeys.KeyboardLayout_KeyNumber9,
[InputKey.Tilde] = LocaleKeys.KeyboardLayout_KeyTilde,
[InputKey.Grave] = LocaleKeys.KeyboardLayout_KeyGrave,
[InputKey.Minus] = LocaleKeys.KeyboardLayout_KeyMinus,
[InputKey.Plus] = LocaleKeys.KeyboardLayout_KeyPlus,
[InputKey.BracketLeft] = LocaleKeys.KeyboardLayout_KeyBracketLeft,
[InputKey.BracketRight] = LocaleKeys.KeyboardLayout_KeyBracketRight,
[InputKey.Semicolon] = LocaleKeys.KeyboardLayout_KeySemicolon,
[InputKey.Quote] = LocaleKeys.KeyboardLayout_KeyQuote,
[InputKey.Comma] = LocaleKeys.KeyboardLayout_KeyComma,
[InputKey.Period] = LocaleKeys.KeyboardLayout_KeyPeriod,
[InputKey.Slash] = LocaleKeys.KeyboardLayout_KeySlash,
[InputKey.BackSlash] = LocaleKeys.KeyboardLayout_KeyBackSlash,
};
public static bool TryGetSemanticLabel(InputKey key, out string label)
{
if (TryGetSemanticLocaleKey(key, out LocaleKeys localeKey))
{
label = GetLocalizedString(localeKey);
return true;
}
label = string.Empty;
return false;
}
public static bool TryGetPhysicalLabel(ConfigPhysicalKey key, out string label)
{
if (TryGetPhysicalLocaleKey(key, out LocaleKeys localeKey))
{
label = GetLocalizedString(localeKey);
return true;
}
label = string.Empty;
return false;
}
public static bool TryGetPhysicalLocaleKey(ConfigPhysicalKey key, out LocaleKeys localeKey)
{
return _sharedLocalizedKeysMap.TryGetValue((InputKey)(int)key, out localeKey);
}
private static bool TryGetSemanticLocaleKey(InputKey key, out LocaleKeys localeKey)
{
return _sharedLocalizedKeysMap.TryGetValue(key, out localeKey) ||
_semanticPrintableKeysMap.TryGetValue(key, out localeKey);
}
private static string GetLocalizedString(LocaleKeys localeKey)
{
if (OperatingSystem.IsMacOS())
{
localeKey = localeKey switch
{
LocaleKeys.KeyboardLayout_KeyControlLeft => LocaleKeys.KeyboardLayout_KeyMacControlLeft,
LocaleKeys.KeyboardLayout_KeyControlRight => LocaleKeys.KeyboardLayout_KeyMacControlRight,
LocaleKeys.KeyboardLayout_KeyAltLeft => LocaleKeys.KeyboardLayout_KeyMacAltLeft,
LocaleKeys.KeyboardLayout_KeyAltRight => LocaleKeys.KeyboardLayout_KeyMacAltRight,
LocaleKeys.KeyboardLayout_KeyWinLeft => LocaleKeys.KeyboardLayout_KeyMacWinLeft,
LocaleKeys.KeyboardLayout_KeyWinRight => LocaleKeys.KeyboardLayout_KeyMacWinRight,
_ => localeKey
};
}
return LocaleManager.Instance[localeKey];
}
}
}

View File

@@ -0,0 +1,234 @@
using Avalonia.Input;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using InputKey = Ryujinx.Input.Key;
namespace Ryujinx.Ava.UI.Helpers
{
internal static class PhysicalKeyLabelHelper
{
private const string ObservedLabelsFileName = "keyboard_layout_labels.json";
private static readonly ConcurrentDictionary<ConfigPhysicalKey, string> _observedLayoutLabels = new();
private static readonly object _observedLayoutLabelsLock = new();
private static readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
private static bool _observedLayoutLabelsLoaded;
public static event Action LabelsChanged;
public static string GetDisplayString(ConfigPhysicalKey key)
{
EnsureObservedLayoutLabelsLoaded();
if (KeyboardLayoutLocaleHelper.TryGetPhysicalLabel(key, out string localizedLabel))
{
return localizedLabel;
}
if (_observedLayoutLabels.TryGetValue(key, out string observedLabel))
{
return observedLabel;
}
if (TryGetFallbackPrintableKeyLabel(key, out string label))
{
return label;
}
return key.ToString();
}
public static void ObserveKeyPress(object sender, KeyEventArgs args)
{
EnsureObservedLayoutLabelsLoaded();
if (args.KeyModifiers != KeyModifiers.None)
{
return;
}
InputKey inputKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey);
if (!TryConvertToConfigPhysicalKey(inputKey, out ConfigPhysicalKey physicalKey) ||
KeyboardLayoutLocaleHelper.TryGetPhysicalLocaleKey(physicalKey, out _))
{
return;
}
if (TryNormalizeObservedPrintableLabel(args.KeySymbol, out string label))
{
if (IsCapsLockOn() && !char.IsLetter(label[0]))
{
return;
}
if (_observedLayoutLabels.TryGetValue(physicalKey, out string existingLabel) && existingLabel == label)
{
return;
}
_observedLayoutLabels[physicalKey] = label;
SaveObservedLayoutLabels();
LabelsChanged?.Invoke();
}
}
private static void EnsureObservedLayoutLabelsLoaded()
{
if (_observedLayoutLabelsLoaded)
{
return;
}
lock (_observedLayoutLabelsLock)
{
if (_observedLayoutLabelsLoaded)
{
return;
}
string labelsPath = GetObservedLabelsPath();
if (!File.Exists(labelsPath))
{
_observedLayoutLabelsLoaded = true;
return;
}
try
{
string labelsJson = File.ReadAllText(labelsPath);
Dictionary<string, string>? labels = JsonSerializer.Deserialize<Dictionary<string, string>>(labelsJson, _serializerOptions);
if (labels != null)
{
foreach ((string key, string value) in labels)
{
if (Enum.TryParse(key, out ConfigPhysicalKey physicalKey) &&
!string.IsNullOrEmpty(value))
{
_observedLayoutLabels[physicalKey] = value;
}
}
}
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.UI, $"Unable to load observed keyboard layout labels from '{labelsPath}': {ex.Message}");
}
finally
{
_observedLayoutLabelsLoaded = true;
}
}
}
private static void SaveObservedLayoutLabels()
{
lock (_observedLayoutLabelsLock)
{
try
{
Dictionary<string, string> labels = new();
foreach ((ConfigPhysicalKey key, string value) in _observedLayoutLabels)
{
labels[key.ToString()] = value;
}
File.WriteAllText(GetObservedLabelsPath(), JsonSerializer.Serialize(labels, _serializerOptions));
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.UI, $"Unable to save observed keyboard layout labels: {ex.Message}");
}
}
}
private static string GetObservedLabelsPath()
{
return Path.Combine(AppDataManager.BaseDirPath, ObservedLabelsFileName);
}
private static bool TryGetFallbackPrintableKeyLabel(ConfigPhysicalKey key, out string label)
{
// The legacy enum name for the ISO extra key is misleading, so give it a distinct physical label.
if (key == ConfigPhysicalKey.Grave)
{
label = "<>";
return true;
}
if (!AvaloniaKeyboardMappingHelper.TryGetAvaPhysicalKey((InputKey)(int)key, out AvaPhysicalKey avaPhysicalKey))
{
label = string.Empty;
return false;
}
label = PhysicalKeyExtensions.ToQwertyKeySymbol(avaPhysicalKey, false);
if (string.IsNullOrEmpty(label) || label.Length != 1 || char.IsControl(label[0]))
{
label = string.Empty;
return false;
}
if (char.IsLetter(label[0]))
{
label = char.ToUpperInvariant(label[0]).ToString();
}
return true;
}
private static bool IsCapsLockOn()
{
try
{
return OperatingSystem.IsWindows() && Console.CapsLock;
}
catch (Exception ex)
{
Logger.Debug?.Print(LogClass.UI, $"CapsLock state query failed: {ex.Message}");
return false;
}
}
private static bool TryNormalizeObservedPrintableLabel(string keySymbol, out string label)
{
if (string.IsNullOrEmpty(keySymbol) || keySymbol.Length != 1 || char.IsControl(keySymbol[0]))
{
label = string.Empty;
return false;
}
label = char.IsLetter(keySymbol[0])
? char.ToUpperInvariant(keySymbol[0]).ToString()
: keySymbol;
return true;
}
private static bool TryConvertToConfigPhysicalKey(InputKey key, out ConfigPhysicalKey physicalKey)
{
if (key is >= InputKey.Unknown and < InputKey.Count)
{
physicalKey = (ConfigPhysicalKey)(int)key;
return true;
}
physicalKey = ConfigPhysicalKey.Unknown;
return false;
}
}
}

View File

@@ -63,6 +63,18 @@ namespace Ryujinx.Ava.UI.Helpers
}
}
[StructLayout(LayoutKind.Sequential)]
public struct NativeRect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width => Right - Left;
public int Height => Bottom - Top;
}
public static nint CreateEmptyCursor()
{
return CreateCursor(nint.Zero, 0, 0, 1, 1, [0xFF], [0x00]);
@@ -119,6 +131,10 @@ 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)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetWindowRect(nint hWnd, out NativeRect lpRect);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool SetWindowPos(

View File

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

View File

@@ -1,5 +1,6 @@
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Threading;
@@ -117,6 +118,11 @@ namespace Ryujinx.Ava.UI.Models.Input
public void UpdateConfig(object config)
{
KeyboardConfig = null;
GamepadConfig = null;
UiStickLeft = (0f, 0f);
UiStickRight = (0f, 0f);
if (config is ControllerInputViewModel padConfig)
{
GamepadConfig = padConfig.Config;
@@ -145,76 +151,86 @@ namespace Ryujinx.Ava.UI.Models.Input
leftBuffer = (0f, 0f);
rightBuffer = (0f, 0f);
switch (Type)
try
{
case DeviceType.Keyboard:
IKeyboard keyboard = (IKeyboard)Parent.AvaloniaKeyboardDriver.GetGamepad("0");
switch (Type)
{
case DeviceType.Keyboard:
IKeyboard keyboard = Parent?.AvaloniaKeyboardDriver?.GetGamepad("0") as IKeyboard;
if (keyboard != null)
{
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
if (keyboard != null && KeyboardConfig != null)
{
leftBuffer.Item1 += 1;
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
if (snapshot.IsPressed(KeyboardConfig.LeftStickRight))
{
leftBuffer.Item1 += 1;
}
if (snapshot.IsPressed(KeyboardConfig.LeftStickLeft))
{
leftBuffer.Item1 -= 1;
}
if (snapshot.IsPressed(KeyboardConfig.LeftStickUp))
{
leftBuffer.Item2 += 1;
}
if (snapshot.IsPressed(KeyboardConfig.LeftStickDown))
{
leftBuffer.Item2 -= 1;
}
if (snapshot.IsPressed(KeyboardConfig.RightStickRight))
{
rightBuffer.Item1 += 1;
}
if (snapshot.IsPressed(KeyboardConfig.RightStickLeft))
{
rightBuffer.Item1 -= 1;
}
if (snapshot.IsPressed(KeyboardConfig.RightStickUp))
{
rightBuffer.Item2 += 1;
}
if (snapshot.IsPressed(KeyboardConfig.RightStickDown))
{
rightBuffer.Item2 -= 1;
}
UiStickLeft = leftBuffer;
UiStickRight = rightBuffer;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
break;
case DeviceType.Controller:
IGamepad controller = Parent?.SelectedGamepad;
if (controller is IKeyboard)
{
leftBuffer.Item1 -= 1;
}
else if (controller != null && GamepadConfig != null)
{
leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick);
rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
{
leftBuffer.Item2 += 1;
}
break;
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
{
leftBuffer.Item2 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
{
rightBuffer.Item1 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
{
rightBuffer.Item1 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
{
rightBuffer.Item2 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
{
rightBuffer.Item2 -= 1;
}
UiStickLeft = leftBuffer;
UiStickRight = rightBuffer;
}
break;
case DeviceType.Controller:
IGamepad controller = Parent.SelectedGamepad;
if (controller != null)
{
leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick);
rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
}
break;
case DeviceType.None:
break;
default:
throw new ArgumentException($"Unable to poll device type \"{Type}\"");
case DeviceType.None:
break;
default:
throw new ArgumentException($"Unable to poll device type \"{Type}\"");
}
}
catch (Exception ex) when (ex is ObjectDisposedException || ex is NullReferenceException || ex is NotSupportedException)
{
Logger.Debug?.Print(LogClass.UI, $"StickVisualizer polling failed: {ex}");
}
UiStickLeft = leftBuffer;

View File

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

View File

@@ -13,6 +13,7 @@ using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using static Ryujinx.Ava.Utilities.StorageProviderExtensions;
namespace Ryujinx.Ava.UI.ViewModels
{
@@ -128,7 +129,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public async void Add()
{
IReadOnlyList<IStorageFile> result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
IReadOnlyList<IStorageFile> result = await CoreDumpable(() => _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.SelectDlcDialogTitle],
AllowMultiple = true,
@@ -141,7 +142,7 @@ namespace Ryujinx.Ava.UI.ViewModels
MimeTypes = ["application/x-nx-nsp"],
},
},
});
}));
int totalDlcAdded = 0;
foreach (IStorageFile file in result)
@@ -250,7 +251,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private Task ShowNewDlcAddedDialog(int numAdded)
{
string msg = string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowDlcAddedMessage], numAdded);
string msg = string.Format(LocaleManager.Instance[LocaleKeys.Dialog_ContentLoading_DLCAddedMessage], numAdded);
return Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.ShowTextDialog(

View File

@@ -2,6 +2,7 @@ using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Views.Input;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Utilities;
using Ryujinx.UI.Views.Input;
@@ -59,48 +60,62 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public partial SvgImage Image { get; set; }
public InputViewModel ParentModel { get; }
private readonly RefEvent<System.Drawing.Color>.Handler _rainbowLedHandler;
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config, StickVisualizer visualizer)
{
ParentModel = model;
Visualizer = visualizer;
_rainbowLedHandler = SetRainbowLed;
model.NotifyChangesEvent += OnParentModelChanged;
OnParentModelChanged();
config.PropertyChanged += (_, args) =>
{
if (args.PropertyName is nameof(Config.UseRainbowLed))
{
if (Config is { UseRainbowLed: true, TurnOffLed: false, EnableLedChanging: true })
Rainbow.Updated += (ref color) => ParentModel.SelectedGamepad.SetLed((uint)color.ToArgb());
else
{
Rainbow.Reset();
if (Config.TurnOffLed)
ParentModel.SelectedGamepad.ClearLed();
else
ParentModel.SelectedGamepad.SetLed(Config.LedColor.ToUInt32());
}
}
};
config.PropertyChanged += OnConfigPropertyChanged;
Config = config;
}
private void OnConfigPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs args)
{
if (args.PropertyName is nameof(Config.UseRainbowLed))
{
if (Config is { UseRainbowLed: true, TurnOffLed: false, EnableLedChanging: true })
{
Rainbow.Updated -= _rainbowLedHandler;
Rainbow.Updated += _rainbowLedHandler;
}
else
{
Rainbow.Reset();
if (Config.TurnOffLed)
ParentModel.SelectedGamepad.ClearLed();
else
ParentModel.SelectedGamepad.SetLed(Config.LedColor.ToUInt32());
}
}
}
private void SetRainbowLed(ref System.Drawing.Color color)
{
ParentModel.SelectedGamepad.SetLed((uint)color.ToArgb());
}
public async void ShowMotionConfig()
{
await MotionInputView.Show(this);
ParentModel.IsModified = true;
ParentModel.RefreshModifiedState();
}
public async void ShowRumbleConfig()
{
await RumbleInputView.Show(this);
ParentModel.IsModified = true;
ParentModel.RefreshModifiedState();
}
public async void ShowLedConfig()
{
await LedInputView.Show(this);
ParentModel.IsModified = true;
ParentModel.RefreshModifiedState();
}
public void OnParentModelChanged()

File diff suppressed because it is too large Load Diff

View File

@@ -136,9 +136,6 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] public partial float VolumeBeforeMute { get; set; }
[ObservableProperty]
public partial bool AreMimeTypesRegistered { get; set; } = FileAssociationHelper.AreMimeTypesRegistered;
[ObservableProperty] public partial Cursor Cursor { get; set; }
[ObservableProperty] public partial string Title { get; set; }
@@ -219,6 +216,11 @@ namespace Ryujinx.Ava.UI.ViewModels
_rendererWaitEvent = new AutoResetEvent(false);
LocaleManager.Instance.PropertyChanged += (sender, args) =>
{
RefreshFileTypeAssociationToggle();
};
if (Program.PreviewerDetached)
{
LoadConfigurableHotKeys();
@@ -679,11 +681,6 @@ namespace Ryujinx.Ava.UI.ViewModels
get => ConsoleHelper.SetConsoleWindowStateSupported;
}
public bool ManageFileTypesVisible
{
get => FileAssociationHelper.IsTypeAssociationSupported;
}
public Glyph Glyph
{
get => (Glyph)ConfigurationState.Instance.UI.GameListViewMode.Value;
@@ -941,6 +938,71 @@ namespace Ryujinx.Ava.UI.ViewModels
return false;
}
public bool FileTypeAssociationsVisible
{
get => FileAssociationHelper.IsTypeAssociationSupported;
}
private void RefreshFileTypeAssociationToggle()
{
OnPropertyChanged(nameof(FileTypeAssociationsMenuHeader));
OnPropertyChanged(nameof(FileTypeAssociationsIcon));
}
private bool _areMimeTypesRegistered = FileAssociationHelper.AreMimeTypesRegistered;
public bool AreMimeTypesRegistered
{
get => _areMimeTypesRegistered;
set
{
if (_areMimeTypesRegistered != value)
{
_areMimeTypesRegistered = value;
RefreshFileTypeAssociationToggle();
}
}
}
public string FileTypeAssociationsMenuHeader =>
AreMimeTypesRegistered
? LocaleManager.Instance[LocaleKeys.MenuBar_File_RemoveFileTypeAssociationsButton]
: LocaleManager.Instance[LocaleKeys.MenuBar_File_AssociateFileTypesButton];
public string FileTypeAssociationsIcon =>
AreMimeTypesRegistered
? "fa-solid fa-link-slash"
: "fa-solid fa-link";
[RelayCommand]
private async Task ToggleFileTypeAssociations()
{
if (AreMimeTypesRegistered)
await RemoveFileTypeAssociations();
else
await AssociateFileTypes();
}
[RelayCommand]
private async Task AssociateFileTypes()
{
AreMimeTypesRegistered = FileAssociationHelper.Install();
if (AreMimeTypesRegistered)
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.Dialog_FileTypeAssociations_AssociationSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
else
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.Dialog_FileTypeAssociations_AssociationFailedMessage]);
}
[RelayCommand]
private async Task RemoveFileTypeAssociations()
{
AreMimeTypesRegistered = !FileAssociationHelper.Uninstall();
if (!AreMimeTypesRegistered)
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.Dialog_FileTypeAssociations_RemoveAssociationSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
else
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.Dialog_FileTypeAssociations_RemoveAssociationFailedMessage]);
}
public async Task HandleFirmwareInstallation(string filename)
{
try
@@ -1407,10 +1469,6 @@ namespace Ryujinx.Ava.UI.ViewModels
public void HideUi() => ShowMenuAndStatusBar = false;
public void ToggleStartGamesInFullscreen() => StartGamesInFullscreen = !StartGamesInFullscreen;
public void ToggleStartGamesWithoutUi() => StartGamesWithoutUi = !StartGamesWithoutUi;
public void ToggleShowConsole() => ShowConsole = !ShowConsole;
public void SetListMode() => Glyph = Glyph.List;
@@ -1601,11 +1659,14 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost.Device.System.SimulateWakeUpMessage();
}
public async Task OpenFile()
public KeyGesture LoadApplicationFromFileGesture => KeyGesture.Parse(OperatingSystem.IsMacOS() ? "Cmd+O" : "Ctrl+O");
public KeyGesture LoadUnpackedGameFromFolderFileGesture => KeyGesture.Parse(OperatingSystem.IsMacOS() ? "Cmd+Shift+O" : "Ctrl+Shift+O");
public async Task LoadApplicationFromFile()
{
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.LoadApplicationFromFileDialogTitle],
Title = LocaleManager.Instance[LocaleKeys.Dialog_FileMenu_LoadApplicationFromFileFilePickerTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
@@ -1671,35 +1732,17 @@ namespace Ryujinx.Ava.UI.ViewModels
else
{
await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.MenuBarFileOpenFromFileError]);
LocaleManager.Instance[LocaleKeys.Error_NoApplicationFoundInFile]);
}
}
}
public async Task LoadDlcFromFolder()
{
await LoadContentFromFolder(
LocaleKeys.AutoloadDlcAddedMessage,
LocaleKeys.AutoloadDlcRemovedMessage,
ApplicationLibrary.AutoLoadDownloadableContents,
LocaleKeys.LoadDLCFromFolderDialogTitle);
}
public async Task LoadTitleUpdatesFromFolder()
{
await LoadContentFromFolder(
LocaleKeys.AutoloadUpdateAddedMessage,
LocaleKeys.AutoloadUpdateRemovedMessage,
ApplicationLibrary.AutoLoadTitleUpdates,
LocaleKeys.LoadTitleUpdatesFromFolderDialogTitle);
}
public async Task OpenFolder()
public async Task LoadUnpackedGameFromFolder()
{
Optional<IStorageFolder> result = await StorageProvider.OpenSingleFolderPickerAsync(
new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.LoadUnpackedGameFromFolderDialogTitle]
Title = LocaleManager.Instance[LocaleKeys.Dialog_FileMenu_LoadUnpackedApplicationFromFolderFilePickerTitle]
});
if (result.TryGet(out IStorageFolder value))
@@ -1713,6 +1756,36 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
private async Task<IReadOnlyList<string>?> PickFolders(LocaleKeys titleKey)
{
return (await StorageProvider.OpenMultiFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[titleKey]
})).TryGet(out IReadOnlyList<IStorageFolder> folders)
? folders.Select(f => f.Path.LocalPath).ToList()
: null;
}
public async Task LoadTitleUpdatesAndDLCFromFolder()
{
if (await PickFolders(LocaleKeys.Dialog_FileMenu_LoadUpdatesAndDLCFromFolderFilePickerTitle) is not { } dirs)
return;
int updAdded = ApplicationLibrary.AutoLoadTitleUpdates(dirs.ToList(), out _);
int dlcAdded = ApplicationLibrary.AutoLoadDownloadableContents(dirs.ToList(), out _);
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await ContentDialogHelper.ShowTextDialog(
LocaleManager.Instance[LocaleKeys.RyujinxConfirm],
string.Format(LocaleManager.Instance[LocaleKeys.Dialog_ContentLoading_UpdatesAddedMessage], updAdded) + "\n\n" +
string.Format(LocaleManager.Instance[LocaleKeys.Dialog_ContentLoading_DLCAddedMessage], dlcAdded),
string.Empty, string.Empty, string.Empty,
LocaleManager.Instance[LocaleKeys.InputDialogOk],
(int)Symbol.Checkmark);
});
}
public static bool InitializeUserConfig(ApplicationData application)
{
// Code where conditions will be met before loading the user configuration (Global Config)
@@ -2060,6 +2133,12 @@ namespace Ryujinx.Ava.UI.ViewModels
}
private nint _savedWindowStyle;
private WindowState _savedWindowState;
private PixelPoint _savedWindowPosition;
private double _savedWindowWidth;
private double _savedWindowHeight;
private Win32NativeInterop.NativeRect _savedWindowRect;
private bool _savedWindowRectValid;
[SupportedOSPlatform("windows")]
private void MakeWindowFullscreen()
@@ -2067,19 +2146,36 @@ namespace Ryujinx.Ava.UI.ViewModels
nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero;
if (hwnd == nint.Zero) return;
PixelPoint windowCenter = new(
Window.Position.X + (int)(Window.Bounds.Width / 2),
Window.Position.Y + (int)(Window.Bounds.Height / 2));
Avalonia.Platform.Screen? screen =
Window.Screens.ScreenFromVisual(Window) ??
Window.Screens.ScreenFromPoint(windowCenter) ??
Window.Screens.Primary;
if (screen == null)
{
return; // Can't determine screen size, don't attempt fullscreen
}
// Save current style and placement
_savedWindowStyle = Win32NativeInterop.GetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE);
_savedWindowState = WindowState;
_savedWindowPosition = Window.Position;
_savedWindowWidth = Window.Width;
_savedWindowHeight = Window.Height;
_savedWindowRectValid = Win32NativeInterop.GetWindowRect(hwnd, out _savedWindowRect);
// Remove window chrome: WS_OVERLAPPEDWINDOW -> WS_POPUP | WS_VISIBLE
Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE,
unchecked((nint)(Win32NativeInterop.WS_POPUP | Win32NativeInterop.WS_VISIBLE)));
// TODO: why is this nullable
Avalonia.Platform.Screen? screen = Window.Screens.ScreenFromVisual(Window);
int w = screen?.Bounds.Width ?? 0;
int h = screen?.Bounds.Height ?? 0;
Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, w, h,
int w = screen.Bounds.Width;
int h = screen.Bounds.Height;
Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, screen.Bounds.X, screen.Bounds.Y, w, h,
Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | Win32NativeInterop.SWP_FRAMECHANGED);
WindowState = WindowState.FullScreen;
@@ -2094,10 +2190,34 @@ namespace Ryujinx.Ava.UI.ViewModels
// Restore original window style
Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE, _savedWindowStyle);
Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, 0, 0,
Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE |
Win32NativeInterop.SWP_FRAMECHANGED | Win32NativeInterop.SWP_NOMOVE | Win32NativeInterop.SWP_NOSIZE);
if (_savedWindowState is WindowState.Maximized)
{
Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, 0, 0,
Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE |
Win32NativeInterop.SWP_FRAMECHANGED | Win32NativeInterop.SWP_NOMOVE | Win32NativeInterop.SWP_NOSIZE);
}
else if (_savedWindowRectValid)
{
Dispatcher.UIThread.Post(() => RestoreSavedWindowRect(hwnd), DispatcherPriority.Background);
}
else
{
Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, 0, 0, 0, 0,
Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE |
Win32NativeInterop.SWP_FRAMECHANGED | Win32NativeInterop.SWP_NOMOVE | Win32NativeInterop.SWP_NOSIZE);
}
}
[SupportedOSPlatform("windows")]
private void RestoreSavedWindowRect(nint hwnd)
{
Window.Position = _savedWindowPosition;
Window.Width = _savedWindowWidth;
Window.Height = _savedWindowHeight;
Win32NativeInterop.SetWindowPos(hwnd, nint.Zero, _savedWindowRect.Left, _savedWindowRect.Top,
_savedWindowRect.Width, _savedWindowRect.Height,
Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | Win32NativeInterop.SWP_FRAMECHANGED);
}
public static void SaveConfig()

View File

@@ -17,6 +17,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using static Ryujinx.Ava.Utilities.StorageProviderExtensions;
namespace Ryujinx.Ava.UI.ViewModels
{
@@ -288,11 +289,11 @@ namespace Ryujinx.Ava.UI.ViewModels
public async void Add()
{
IReadOnlyList<IStorageFolder> result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
IReadOnlyList<IStorageFolder> result = await CoreDumpable(() => _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.SelectModDialogTitle],
AllowMultiple = true,
});
}));
foreach (IStorageFolder folder in result)
{

View File

@@ -109,6 +109,31 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool StartGamesInFullscreen
{
get => ConfigurationState.Instance.UI.StartFullscreen;
set
{
if (ConfigurationState.Instance.UI.StartFullscreen.Value != value)
{
ConfigurationState.Instance.UI.StartFullscreen.Value = value;
OnPropertyChanged();
}
}
}
public bool StartGamesWithoutUi
{
get => ConfigurationState.Instance.UI.StartNoUI;
set
{
if (ConfigurationState.Instance.UI.StartNoUI.Value != value)
{
ConfigurationState.Instance.UI.StartNoUI.Value = value;
OnPropertyChanged();
}
}
}
public int GraphicsBackendMultithreadingIndex
{
get;
@@ -638,6 +663,8 @@ namespace Ryujinx.Ava.UI.ViewModels
HideCursor = (int)config.HideCursor.Value;
UpdateCheckerType = (int)config.UpdateCheckerType.Value;
FocusLostActionType = (int)config.FocusLostActionType.Value;
StartGamesInFullscreen = config.UI.StartFullscreen;
StartGamesWithoutUi = config.UI.StartNoUI;
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
@@ -758,6 +785,8 @@ namespace Ryujinx.Ava.UI.ViewModels
config.FocusLostActionType.Value = (FocusLostType)FocusLostActionType;
config.UI.GameDirs.Value = [.. GameDirectories];
config.UI.AutoloadDirs.Value = [.. AutoloadDirectories];
config.UI.StartFullscreen.Value = StartGamesInFullscreen;
config.UI.StartNoUI.Value = StartGamesWithoutUi;
config.UI.BaseStyle.Value = BaseStyleIndex switch
{

View File

@@ -11,6 +11,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using static Ryujinx.Ava.Utilities.StorageProviderExtensions;
namespace Ryujinx.Ava.UI.ViewModels
{
@@ -148,7 +149,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public async Task Add()
{
IReadOnlyList<IStorageFile> result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
IReadOnlyList<IStorageFile> result = await CoreDumpable(() => _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
AllowMultiple = true,
FileTypeFilter = new List<FilePickerFileType>
@@ -160,7 +161,7 @@ namespace Ryujinx.Ava.UI.ViewModels
MimeTypes = ["application/x-nx-nsp"],
},
},
});
}));
int totalUpdatesAdded = 0;
foreach (IStorageFile file in result)
@@ -187,7 +188,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private Task<UserResult> ShowNewUpdatesAddedDialog(int numAdded)
{
string msg = string.Format(LocaleManager.Instance[LocaleKeys.UpdateWindowUpdateAddedMessage], numAdded);
string msg = string.Format(LocaleManager.Instance[LocaleKeys.Dialog_ContentLoading_UpdatesAddedMessage], numAdded);
return Dispatcher.UIThread.InvokeAsync(async () =>
await ContentDialogHelper.ShowTextDialog(
LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle],

View File

@@ -104,7 +104,7 @@ namespace Ryujinx.Ava.UI.Views.Input
PointerPressed += MouseClick;
ControllerInputViewModel viewModel = (DataContext as ControllerInputViewModel);
ControllerInputViewModel viewModel = ViewModel;
IKeyboard keyboard =
(IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver
@@ -113,10 +113,9 @@ namespace Ryujinx.Ava.UI.Views.Input
_currentAssigner.ButtonAssigned += (sender, e) =>
{
if (e.ButtonValue.HasValue)
if (e.ButtonValue.HasValue && IsActiveAssignmentContext(viewModel))
{
Button buttonValue = e.ButtonValue.Value;
FlagInputConfigChanged();
switch (button.Name)
{
@@ -187,6 +186,8 @@ namespace Ryujinx.Ava.UI.Views.Input
viewModel.Config.RightJoystick = buttonValue.AsHidType<StickInputId>();
break;
}
FlagInputConfigChanged();
}
};
@@ -212,7 +213,15 @@ namespace Ryujinx.Ava.UI.Views.Input
private void FlagInputConfigChanged()
{
(DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true;
if (DataContext is ControllerInputViewModel viewModel && VisualRoot is not null)
{
viewModel.ParentModel.RefreshModifiedState();
}
}
private bool IsActiveAssignmentContext(ControllerInputViewModel viewModel)
{
return VisualRoot is not null && ReferenceEquals(DataContext, viewModel);
}
private void MouseClick(object sender, PointerPressedEventArgs e)

View File

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

View File

@@ -1,4 +1,8 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia;
using Avalonia.Layout;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
@@ -15,9 +19,14 @@ namespace Ryujinx.Ava.UI.Views.Input
public InputView()
{
ViewModel = new InputViewModel(this, ConfigurationState.Instance.System.UseInputGlobalConfig);
ReplaceViewModel(ConfigurationState.Instance.System.UseInputGlobalConfig);
}
InitializeComponent();
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
ViewModel?.RetargetKeyboardDriver(this);
}
public void SaveCurrentProfile()
@@ -28,8 +37,18 @@ namespace Ryujinx.Ava.UI.Views.Input
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
{
Dispose();
ViewModel = new InputViewModel(this, enableConfigGlobal); // Create new Input Page with global input configs
ReplaceViewModel(enableConfigGlobal);
}
private void ReplaceViewModel(bool useGlobalConfig)
{
ViewModel = new InputViewModel(this, useGlobalConfig); // Create new Input Page with the selected input config scope.
InitializeComponent();
if (VisualRoot is not null)
{
ViewModel.RetargetKeyboardDriver(this);
}
}
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -83,7 +102,56 @@ namespace Ryujinx.Ava.UI.Views.Input
if (sender is FAComboBox faComboBox)
{
faComboBox.IsDropDownOpen = false;
ViewModel.IsModified = true;
ViewModel.RefreshModifiedState();
}
}
private async void ResetCurrentDeviceToDefaultsButton_OnClick(object sender, RoutedEventArgs e)
{
if (!ViewModel.NeedsResetCurrentDeviceToDefaultsConfirmation())
{
ViewModel.ResetCurrentDeviceToDefaults();
return;
}
Window owner = TopLevel.GetTopLevel(this) as Window;
StackPanel content = new()
{
Spacing = 4,
MaxWidth = 360,
};
content.Children.Add(new TextBlock
{
Text = LocaleManager.Instance[LocaleKeys.DialogControllerSettingsResetKeybindsConfirmMessage],
TextWrapping = TextWrapping.Wrap,
MaxWidth = 360,
});
content.Children.Add(new TextBlock
{
Text = LocaleManager.Instance[LocaleKeys.DialogControllerSettingsResetKeybindsConfirmSubMessage],
TextWrapping = TextWrapping.Wrap,
MaxWidth = 360,
});
ContentDialog contentDialog = new ContentDialog
{
Title = LocaleManager.Instance[LocaleKeys.RyujinxConfirm],
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.InputDialogYes],
CloseButtonText = LocaleManager.Instance[LocaleKeys.InputDialogNo],
DefaultButton = ContentDialogButton.Primary,
Content = content,
}.ApplyStyles();
ContentDialogResult result = owner is not null
? await contentDialog.ShowAsync(owner)
: await ContentDialogHelper.ShowAsync(contentDialog);
if (result == ContentDialogResult.Primary)
{
ViewModel.ResetCurrentDeviceToDefaults();
}
}

View File

@@ -12,7 +12,7 @@ using Ryujinx.Input.Assigner;
using System;
using System.Collections.Generic;
using Button = Ryujinx.Input.Button;
using Key = Ryujinx.Common.Configuration.Hid.Key;
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Ava.UI.Views.Input
{
@@ -63,105 +63,108 @@ namespace Ryujinx.Ava.UI.Views.Input
PointerPressed += MouseClick;
KeyboardInputViewModel viewModel = ViewModel;
IKeyboard keyboard =
(IKeyboard)ViewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
(IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
IButtonAssigner assigner =
new KeyboardKeyAssigner((IKeyboard)ViewModel.ParentModel.SelectedGamepad);
new KeyboardKeyAssigner((IKeyboard)viewModel.ParentModel.SelectedGamepad);
_currentAssigner.ButtonAssigned += (_, be) =>
{
if (be.ButtonValue.HasValue)
if (be.ButtonValue.HasValue && IsActiveAssignmentContext(viewModel))
{
Button buttonValue = be.ButtonValue.Value;
ViewModel.ParentModel.IsModified = true;
switch (button.Name)
{
case "ButtonZl":
ViewModel.Config.ButtonZl = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonZl = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonL":
ViewModel.Config.ButtonL = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonL = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonMinus":
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonMinus = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickButton":
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<Key>();
viewModel.Config.LeftStickButton = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickUp":
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<Key>();
viewModel.Config.LeftStickUp = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickDown":
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<Key>();
viewModel.Config.LeftStickDown = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickRight":
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<Key>();
viewModel.Config.LeftStickRight = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickLeft":
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<Key>();
viewModel.Config.LeftStickLeft = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadUp":
ViewModel.Config.DpadUp = buttonValue.AsHidType<Key>();
viewModel.Config.DpadUp = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadDown":
ViewModel.Config.DpadDown = buttonValue.AsHidType<Key>();
viewModel.Config.DpadDown = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadLeft":
ViewModel.Config.DpadLeft = buttonValue.AsHidType<Key>();
viewModel.Config.DpadLeft = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadRight":
ViewModel.Config.DpadRight = buttonValue.AsHidType<Key>();
viewModel.Config.DpadRight = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftButtonSr":
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<Key>();
viewModel.Config.LeftButtonSr = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftButtonSl":
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<Key>();
viewModel.Config.LeftButtonSl = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightButtonSr":
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<Key>();
viewModel.Config.RightButtonSr = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightButtonSl":
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<Key>();
viewModel.Config.RightButtonSl = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonZr":
ViewModel.Config.ButtonZr = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonZr = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonR":
ViewModel.Config.ButtonR = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonR = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonPlus":
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonPlus = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonA":
ViewModel.Config.ButtonA = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonA = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonB":
ViewModel.Config.ButtonB = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonB = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonX":
ViewModel.Config.ButtonX = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonX = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonY":
ViewModel.Config.ButtonY = buttonValue.AsHidType<Key>();
viewModel.Config.ButtonY = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickButton":
ViewModel.Config.RightStickButton = buttonValue.AsHidType<Key>();
viewModel.Config.RightStickButton = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickUp":
ViewModel.Config.RightStickUp = buttonValue.AsHidType<Key>();
viewModel.Config.RightStickUp = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickDown":
ViewModel.Config.RightStickDown = buttonValue.AsHidType<Key>();
viewModel.Config.RightStickDown = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickRight":
ViewModel.Config.RightStickRight = buttonValue.AsHidType<Key>();
viewModel.Config.RightStickRight = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickLeft":
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<Key>();
viewModel.Config.RightStickLeft = buttonValue.AsHidType<PhysicalKey>();
break;
}
viewModel.ParentModel.RefreshModifiedState();
}
};
@@ -207,40 +210,40 @@ namespace Ryujinx.Ava.UI.Views.Input
{
Dictionary<string, Action> buttonActions = new()
{
{ "ButtonZl", () => ViewModel.Config.ButtonZl = Key.Unbound },
{ "ButtonL", () => ViewModel.Config.ButtonL = Key.Unbound },
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = Key.Unbound },
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = Key.Unbound },
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = Key.Unbound },
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = Key.Unbound },
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = Key.Unbound },
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = Key.Unbound },
{ "DpadUp", () => ViewModel.Config.DpadUp = Key.Unbound },
{ "DpadDown", () => ViewModel.Config.DpadDown = Key.Unbound },
{ "DpadLeft", () => ViewModel.Config.DpadLeft = Key.Unbound },
{ "DpadRight", () => ViewModel.Config.DpadRight = Key.Unbound },
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = Key.Unbound },
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = Key.Unbound },
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = Key.Unbound },
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = Key.Unbound },
{ "ButtonZr", () => ViewModel.Config.ButtonZr = Key.Unbound },
{ "ButtonR", () => ViewModel.Config.ButtonR = Key.Unbound },
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = Key.Unbound },
{ "ButtonA", () => ViewModel.Config.ButtonA = Key.Unbound },
{ "ButtonB", () => ViewModel.Config.ButtonB = Key.Unbound },
{ "ButtonX", () => ViewModel.Config.ButtonX = Key.Unbound },
{ "ButtonY", () => ViewModel.Config.ButtonY = Key.Unbound },
{ "RightStickButton", () => ViewModel.Config.RightStickButton = Key.Unbound },
{ "RightStickUp", () => ViewModel.Config.RightStickUp = Key.Unbound },
{ "RightStickDown", () => ViewModel.Config.RightStickDown = Key.Unbound },
{ "RightStickRight", () => ViewModel.Config.RightStickRight = Key.Unbound },
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = Key.Unbound }
{ "ButtonZl", () => ViewModel.Config.ButtonZl = PhysicalKey.Unbound },
{ "ButtonL", () => ViewModel.Config.ButtonL = PhysicalKey.Unbound },
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = PhysicalKey.Unbound },
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = PhysicalKey.Unbound },
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = PhysicalKey.Unbound },
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = PhysicalKey.Unbound },
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = PhysicalKey.Unbound },
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = PhysicalKey.Unbound },
{ "DpadUp", () => ViewModel.Config.DpadUp = PhysicalKey.Unbound },
{ "DpadDown", () => ViewModel.Config.DpadDown = PhysicalKey.Unbound },
{ "DpadLeft", () => ViewModel.Config.DpadLeft = PhysicalKey.Unbound },
{ "DpadRight", () => ViewModel.Config.DpadRight = PhysicalKey.Unbound },
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = PhysicalKey.Unbound },
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = PhysicalKey.Unbound },
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = PhysicalKey.Unbound },
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = PhysicalKey.Unbound },
{ "ButtonZr", () => ViewModel.Config.ButtonZr = PhysicalKey.Unbound },
{ "ButtonR", () => ViewModel.Config.ButtonR = PhysicalKey.Unbound },
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = PhysicalKey.Unbound },
{ "ButtonA", () => ViewModel.Config.ButtonA = PhysicalKey.Unbound },
{ "ButtonB", () => ViewModel.Config.ButtonB = PhysicalKey.Unbound },
{ "ButtonX", () => ViewModel.Config.ButtonX = PhysicalKey.Unbound },
{ "ButtonY", () => ViewModel.Config.ButtonY = PhysicalKey.Unbound },
{ "RightStickButton", () => ViewModel.Config.RightStickButton = PhysicalKey.Unbound },
{ "RightStickUp", () => ViewModel.Config.RightStickUp = PhysicalKey.Unbound },
{ "RightStickDown", () => ViewModel.Config.RightStickDown = PhysicalKey.Unbound },
{ "RightStickRight", () => ViewModel.Config.RightStickRight = PhysicalKey.Unbound },
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = PhysicalKey.Unbound }
};
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
{
action();
ViewModel.ParentModel.IsModified = true;
ViewModel.ParentModel.RefreshModifiedState();
}
}
}
@@ -251,5 +254,10 @@ namespace Ryujinx.Ava.UI.Views.Input
_currentAssigner?.Cancel();
_currentAssigner = null;
}
private bool IsActiveAssignmentContext(KeyboardInputViewModel viewModel)
{
return VisualRoot is not null && ReferenceEquals(DataContext, viewModel);
}
}
}

Some files were not shown because too many files have changed in this diff Show More