Compare commits

..

23 Commits

Author SHA1 Message Date
Max
dcac29680a Updated PlayReports for more titles (#93)
- Echoes of Wisdom (Warps)
- Super Mario Odyssey (Kingdoms)
- Super Mario Bros. Wonder (World & Course)
- Pokemon Scarlet/Violet (DLC & Accademy Rooms)
- Super Mario 3D All Stars (Game Selection) (Berry is working on track showcase)

Co-authored-by: berrydiaboli <anthonyhoffman54444@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/93
2026-05-14 04:06:28 +00:00
Max
ec07e51807 Update compatibility.csv (#94)
- Emio – The Smiling Man: Famicom Detective Club (DEMO)
- GROOVE COASTER WAI WAI PARTY!!!!
- Metroid Prime 4: Beyond
- Mute Crimson DX
- Pokémon Champions
- Pokémon FireRed Version
- Pokémon LeafGreen Version
- Tomodachi Life: Living the Dream
- Tomodachi Life: Living the Dream – Welcome Edtion

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/94
2026-05-14 04:05:58 +00:00
cookieso
e7aa6775af Input: Implement HD Rumble for compatible Nin devices (#40)
This PR addresses [this issue](https://github.com/Ryubing/Issues/issues/231) and implements HD Rumble for compatible Nin devices.
## New Features
- Add the option for Gamepads to implement HD Rumble
- Add the HD rumble capability to SDL3Gamepad and SDL3JoyCon when they meet certain requirements

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/40
2026-05-13 23:57:30 +00:00
Renovate Bot
49891ba7af Update avalonia monorepo to 11.3.15 (#85)
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.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia/11.3.14/11.3.15?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.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Desktop/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Desktop/11.3.14/11.3.15?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.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Diagnostics/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Diagnostics/11.3.14/11.3.15?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.14` → `11.3.15` | ![age](https://developer.mend.io/api/mc/badges/age/nuget/Avalonia.Markup.Xaml.Loader/11.3.15?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Avalonia.Markup.Xaml.Loader/11.3.14/11.3.15?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 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:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjUuMSIsInVwZGF0ZWRJblZlciI6IjQzLjE2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbXX0=-->

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/85
2026-05-12 02:41:10 +00:00
Max
89ea41ef84 Check if the Device is rendering before waiting on it (#86)
Fixes an issue where there were missed references and an ``OperationCancelled`` exception when exiting an application.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/86
2026-05-12 02:38:09 +00:00
yell0wsuit
f8167eb625 [HLE] Match hardware screenshot buffer size behavior for captures (#44)
## Description

~~Fixes a fatal CLR crash when `caps` screenshot saving receives an input buffer larger than `0x384000`.~~

~~Resolves a crash in Tomodachi Life: Living the Dream where saving the pictures to the system's album crashes with 0x80131506.~~

Follow up to #18. This PR adjusts the validation and copy behavior to better match real hardware, and adds logging to make invalid screenshot buffer cases easier to diagnose.

Real hardware accepts screenshot buffers with a size greater than or equal to `0x384000`, but only `0x384000` bytes are needed for the 1280x720 RGBA image.

This changes screenshot saving to:

- reject buffers smaller than `0x384000`
- accept buffers equal to or larger than `0x384000`
- copy only the first `0x384000` bytes into the 1280x720 bitmap

## Testing

Tested with a real Switch NRO using `capssuSaveScreenShotEx0`, `capssuSaveScreenShotEx1`, and `capssuSaveScreenShotEx2`.

Observed hardware behavior:

```text
0x384000     => OK
0x384000 - 1 => NullInputBuffer
0x384000 + 1 => OK
0x3C0000     => OK // Tomo life picture size
```

Co-authored-by: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/44
2026-05-10 23:57:05 +00:00
Babib3l
bab160d650 Fix Windows fullscreen gap when toggling from maximized (#80)
This PR aims to Fix the Windows fullscreen gap when toggling from maximized state by using canonical Win32 fullscreen approach. When entering fullscreen, the window style is saved and replaced with WS_POPUP | WS_VISIBLE to remove all window chrome (title bar, borders), then SetWindowPos sizes the window to cover the full screen with SWP_FRAMECHANGED to force Win32 to recalculate the frame. On exit, the original window style is restored. This eliminates the few-pixel gap at the top that occurred because Avalonia's WindowState = FullScreen transition from a maximized state retained the title bar non-client area, leaving visible space at the top of the screen (Fullscreen resolution would be 1920 x 1072p on a 1080p monitor, it now correctly renders at 1920 x 1080p)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/80
2026-05-10 23:47:39 +00:00
yell0wsuit
e8cc252d9a [HLE] Fix StoreData layout and implement IDatabaseService.Append (#43)
- Fixes `StoreData` layout/update handling so `UpdateLatest` returns the stored Mii data correctly.
- Implements `IDatabaseService.Append` (https://switchbrew.org/wiki/Shared_Database_services#IDatabaseService)

Also adds regression tests for `UpdateLatest` and `Append`.

(Might) fix Mario Kart 8 Deluxe crashing on first boot due to failed Mii verification check (due to custom Mii from emulator), and potentially Tomodachi Life: Living the Dream for the "Import Mii from system" option.

Co-authored-by: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/43
2026-05-10 23:39:19 +00:00
Max
8065dec744 [HLE] Renamed INotificationServicesForSystem and implemented a few commands and stubs (#6)
Should allow Ring Fit and other games that rely heavily on the notification system to get in-game (?), needs testing.

if you're wondering what happened to the first branch -- no you're not. (duplicated history somehow??)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/6
2026-05-10 23:31:45 +00:00
Shyanne
e9c31bea3b [DEBUG] Implemented NetLog logging type (#5)
Implemented a new debug log type called NetLog and added more verbose logging to the LDN service.
I'd like some feedback on what all should be logged under this category -- I'm likely going to be adding logs for sockets as well, but I want to know specifically if the logging should be more or less verbose and what would be the most helpful things to log.

![image](/attachments/70d5d467-2b57-436b-944f-7bf7a1f609af)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/5
2026-05-10 23:29:15 +00:00
Frosch
5511ff5686 fix: gamepads have the same name (#27)
When connecting multiple controllers of the same model, the first device's name ends with (0), the second with (1), the third with (1), the fourth with (1), and so on. To ensure these names are truly unique, GetUniqueGamepadName is now called recursively.

Before:
![image](/attachments/c27ab407-0945-48d8-92a8-6f1fe7fb2727)

After:
![image](/attachments/da7b1427-958c-45d5-8351-6f977d971e1e)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/27
2026-05-10 23:19:59 +00:00
Max
bf7f978f9d [HLE] Implemented ILibraryAppletSelfAccessor:1 (#79)
Needed for Tomodachi Life: Living the Dream (?)

based on [this](https://www.reddit.com/r/Ryubing/comments/1t4lfc9/comment/ok4e7tu/)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/79
2026-05-10 23:18:53 +00:00
Max
1f9bfab923 Updated OpenGL calls to no longer be deprecated (#83)
- updated SharpCompress 0.47.4 -> 0.48.0 (security)
- set ProcessResult to be nullable (since it is)

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/83
2026-05-10 23:18:23 +00:00
greem
5d8cb3e378 This stupid bullshit doesn't work, I'm done 2026-05-10 21:09:26 +00:00
GreemDev
708186d8d2 Revert "Update .forgejo/workflows/build.yml"
This reverts commit 2d2661298c.
2026-05-10 15:56:48 -05:00
GreemDev
ad34237fc6 Revert "use new workflow type in conditions"
This reverts commit bf083a716c.
2026-05-10 15:56:44 -05:00
greem
bf083a716c use new workflow type in conditions 2026-05-05 10:12:06 +00:00
greem
2d2661298c Update .forgejo/workflows/build.yml 2026-05-05 09:29:41 +00:00
greem
c4788154fd even more annoying skill issue! 2026-05-05 09:15:54 +00:00
greem
49dd56953c annoying error 2026-05-05 08:52:08 +00:00
greem
722eb93554 Use a dedicated access token instead of the runner-generated one. 2026-05-05 08:07:38 +00:00
ryuadmin
b0179e6433 [ci skip] Improve logging for PR build comment step. 2026-05-05 07:50:58 +00:00
ryuadmin
1d3d4197b7 fix: *hopefully* fix build comments 2026-05-05 06:22:51 +00:00
51 changed files with 1663 additions and 251 deletions

View File

@@ -202,43 +202,3 @@ jobs:
name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal
path: "publish/*.tar.gz"
if: forgejo.event_name == 'pull_request'
post_comment:
name: Post comment linking uploaded artifacts
runs-on: ubuntu-latest
needs:
- build
- build_macos
steps:
- uses: actions/github-script@v9
with:
script: |
const forgejo = getOctokit(process.env.FORGEJO_TOKEN, {
baseUrl: 'https://git.ryujinx.app/api/v1'
});
const {owner, repo} = context.repo;
const run_id = ${{ forgejo.run_number }};
const issue_number = ${{ forgejo.event.pull_request.number }};
core.info(`Using pull request ${issue_number}`);
const {data: {artifacts}} = await forgejo.rest.actions.listWorkflowRunArtifacts({owner, repo, run_id});
if (artifacts == undefined || !artifacts.length) {
return core.error(`No artifacts found`);
}
let body = `Download the artifacts for this pull request:\n`;
for (const art of artifacts) {
const url = `https://git.ryujinx.app/api/v1/repos/${owner}/${repo}/actions/artifacts/${art.id}/zip`;
body += `\n* [${art.name}](${url})`;
}
const {data: comments} = await forgejo.rest.issues.listComments({repo, owner, issue_number});
const existing_comment = comments.find((c) => c.user.login === 'forgejo-actions');
if (existing_comment) {
core.info(`Updating comment ${existing_comment.id}`);
await forgejo.rest.issues.updateComment({repo, owner, comment_id: existing_comment.id, body});
} else {
core.info(`Creating a comment`);
await forgejo.rest.issues.createComment({repo, owner, issue_number, body});
}

View File

@@ -3,12 +3,12 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.14" />
<PackageVersion Include="Avalonia" Version="11.3.15" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.14" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.14" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.14" />
<PackageVersion Include="SharpCompress" Version="0.47.4" />
<PackageVersion Include="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.0" />
<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" />
@@ -64,4 +64,4 @@
<PackageVersion Include="System.IO.Hashing" Version="9.0.15" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.1.3" />
</ItemGroup>
</Project>
</Project>

View File

@@ -6100,6 +6100,31 @@
"zh_TW": "檔案系統全域存取日誌模式:"
}
},
{
"ID": "SettingsTabLoggingEnableNetLogs",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Enable Net Logs",
"es_ES": "Habilitar registros de red.",
"fr_FR": "",
"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": "SettingsTabLoggingDeveloperOptions",
"Translations": {
@@ -17075,6 +17100,31 @@
"zh_TW": "啟用檔案系統存取日誌輸出到控制台中。可能的模式為 0 到 3。"
}
},
{
"ID": "NetLogTooltip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Prints network log messages in the console.",
"es_ES": "Imprimir registros de red en la consola.",
"fr_FR": "",
"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": "DeveloperOptionTooltip",
"Translations": {

View File

@@ -1061,6 +1061,7 @@
0100BCA016636000,"eBaseball Powerful Pro Yakyuu 2022",gpu;services-horizon;crash,nothing,2024-05-26 23:07:19
01001F20100B8000,"Eclipse: Edge of Light",,playable,2020-08-11 23:06:29
0100E0A0110F4000,"eCrossminton",,playable,2020-07-11 18:24:27
010054601D54C000,"Emio The Smiling Man: Famicom Detective Club (DEMO)",demo,playable,2026-05-13 18:32:12
0100ABE00DB4E000,"Edna & Harvey: Harvey's New Eyes",nvdec,playable,2021-01-26 14:36:08
01004F000B716000,"Edna & Harvey: The Breakout Anniversary Edition",crash;nvdec,ingame,2022-08-01 16:59:56
01002550129F0000,"Effie",,playable,2022-10-27 14:36:39
@@ -1204,7 +1205,7 @@
01003B200E440000,"Five Nights at Freddy's: Sister Location",,playable,2023-10-06 09:00:58
010038200E088000,"Flan",crash;regression,ingame,2021-11-17 07:39:28
01000A0004C50000,"FLASHBACK™",nvdec,playable,2020-05-14 13:57:29
0100C53004C52000,"Flat Heroes",gpu,ingame,2022-07-26 19:37:37
0100C53004C52000,"Flat Heroes",,playable,2026-02-27 17:00:00
0100B54012798000,"Flatland: Prologue",,playable,2020-12-11 20:41:12
0100307004B4C000,"Flinthook",online,playable,2021-03-25 20:42:29
010095A004040000,"Flip Wars",services;ldn-untested,ingame,2022-05-02 15:39:18
@@ -1394,6 +1395,7 @@
0100c3c012718000,"Grand Theft Auto: III The Definitive Edition",gpu;UE4,ingame,2022-10-31 20:13:52
0100182014022000,"Grand Theft Auto: Vice City The Definitive Edition",gpu;UE4,ingame,2022-10-31 20:13:52
010065a014024000,"Grand Theft Auto: San Andreas The Definitive Edition",gpu;UE4,ingame,2022-10-31 20:13:52
0100EB500D92E000,"GROOVE COASTER WAI WAI PARTY!!!!",gpu,ingame,2026-05-13 18:32:12
0100822012D76000,"HAAK",gpu,ingame,2023-02-19 14:31:05
01007E100EFA8000,"Habroxia",,playable,2020-06-16 23:04:42
0100535012974000,"Hades",vulkan,playable,2022-10-05 10:45:21
@@ -1832,6 +1834,7 @@
010055200E87E000,"Metamorphosis",UE4;audout;gpu;nvdec,ingame,2021-06-16 16:18:11
0100D4900E82C000,"Metro 2033 Redux",gpu,ingame,2022-11-09 10:53:13
0100F0400E850000,"Metro: Last Light Redux",slow;nvdec;vulkan-backend-bug,ingame,2023-11-01 11:53:52
010019A01E2F2000,"Metroid Prime 4: Beyond",,ingame,2026-05-13 18:32:12
010012101468C000,"Metroid Prime™ Remastered",gpu;Needs Update;vulkan-backend-bug;opengl-backend-bug,ingame,2024-05-07 22:48:15
010093801237C000,"Metroid™ Dread",,playable,2023-11-13 04:02:36
0100A1200F20C000,"Midnight Evil",,playable,2022-10-18 22:55:19
@@ -1945,6 +1948,7 @@
0100C3E00ACAA000,"Mutant Football League: Dynasty Edition",online-broken,playable,2022-08-05 17:01:51
01004BE004A86000,"Mutant Mudds Collection",,playable,2022-08-05 17:11:38
0100E6B00DEA4000,"Mutant Year Zero: Road to Eden - Deluxe Edition",nvdec;UE4,playable,2022-09-10 13:31:10
010037501F864000,"Mute Crimson DX",,ingame,2026-05-13 18:32:12
0100161009E5C000,"MX Nitro: Unleashed",,playable,2022-09-27 22:34:33
0100218011E7E000,"MX vs ATV All Out",nvdec;UE4;vulkan-backend-bug,playable,2022-10-25 19:51:46
0100D940063A0000,"MXGP3 - The Official Motocross Videogame",UE4;gpu;nvdec,ingame,2020-12-16 14:00:20
@@ -2268,6 +2272,7 @@
010086F0064CE000,"Poi: Explorer Edition",nvdec,playable,2021-01-21 19:32:00
0100EB6012FD2000,"Poison Control",,playable,2021-05-16 14:01:54
010072400E04A000,"Pokémon Café ReMix",,playable,2021-08-17 20:00:04
01005B7008C52800,"Pokémon Champions",Needs Update;services;online-broke,menus,2026-05-13 18:32:12
010008c01e742000,"Pokémon Friends",crash;services,menus,2025-07-24 13:32:00
01003D200BAA2000,"Pokémon Mystery Dungeon™: Rescue Team DX",mac-bug,playable,2024-01-21 00:16:32
01008DB008C2C000,"Pokémon Shield + Pokémon Shield Expansion Pass",deadlock;crash;online-broken;ldn-works;LAN,ingame,2024-08-12 07:20:22
@@ -2275,6 +2280,8 @@
01009AD008C4C000,"Pokémon: Let's Go, Pikachu! demo",slow;demo,playable,2023-11-26 11:23:20
0100000011D90000,"Pokémon™ Brilliant Diamond",gpu;ldn-works,ingame,2024-08-28 13:26:35
010018E011D92000,"Pokémon™ Shining Pearl",gpu;ldn-works,ingame,2024-08-28 13:26:35
100554023408000,"Pokémon FireRed Version",crashes,nothing,2026-05-13 18:32:12
010034D02340E000,"Pokémon LeafGreen Version",crashes,nothing,2026-05-13 18:32:12
010015F008C54000,"Pokémon™ HOME",Needs Update;crash;services,menus,2020-12-06 06:01:51
01001F5010DFA000,"Pokémon™ Legends: Arceus",gpu;Needs Update;ldn-works,ingame,2024-09-19 10:02:02
0100F43008C44000,"Pokémon™ Legends: Z-A",gpu;crash;ldn-works,ingame,2025-11-16 00:30:00
@@ -2866,7 +2873,7 @@
0100277011F1A000,"Super Mario Bros.™ 35",online-broken,menus,2022-08-07 16:27:25
010015100B514000,"Super Mario Bros.™ Wonder",amd-vendor-bug,playable,2024-09-06 13:21:21
01009B90006DC000,"Super Mario Maker™ 2",online-broken;ldn-broken,playable,2024-08-25 11:05:19
0100000000010000,"Super Mario Odyssey™",nvdec;intel-vendor-bug;mac-bug,playable,2024-08-25 01:32:34
0100000000010000,"Super Mario Odyssey™",nvdec;intel-vendor-bug;mac-bug;amd-vendor-bug,playable,2026-05-13 18:32:12
010036B0034E4000,"Super Mario Party™",gpu;Needs Update;ldn-works,ingame,2024-06-21 05:10:16
0100965017338000,"Super Mario Party Jamboree",mac-bug;gpu,ingame,2025-02-17 02:09:20
0100BC0018138000,"Super Mario RPG™",gpu;audio;nvdec,ingame,2024-06-19 17:43:42
@@ -3163,6 +3170,8 @@
0100E2E00CB14000,"Tokyo School Life",,playable,2022-09-16 20:25:54
010024601BB16000,"Tomb Raider I-III Remastered Starring Lara Croft",gpu;opengl,ingame,2024-09-27 12:32:04
0100D7F01E49C000,"Tomba! Special Edition",services-horizon,nothing,2024-09-15 21:59:54
010051F0207B2000,"Tomodachi Life: Living the Dream",amd-vendor-bug;gpu;intel-vendor-bug;ldn-broken,ingame,2026-05-13 18:32:12
0100CA502552A000,"Tomodachi Life: Living the Dream Welcome Edtion",amd-vendor-bug;demo,playable,2026-05-13 18:32:12
0100D400100F8000,"Tonight We Riot",,playable,2021-02-26 15:55:09
0100CC00102B4000,"Tony Hawk's™ Pro Skater™ 1 + 2",gpu;Needs Update,ingame,2024-09-24 08:18:14
010093F00E818000,"Tools Up!",crash,ingame,2020-07-21 12:58:17
1 title_id game_name labels status last_updated
1061 0100BCA016636000 eBaseball Powerful Pro Yakyuu 2022 gpu;services-horizon;crash nothing 2024-05-26 23:07:19
1062 01001F20100B8000 Eclipse: Edge of Light playable 2020-08-11 23:06:29
1063 0100E0A0110F4000 eCrossminton playable 2020-07-11 18:24:27
1064 010054601D54C000 Emio – The Smiling Man: Famicom Detective Club (DEMO) demo playable 2026-05-13 18:32:12
1065 0100ABE00DB4E000 Edna & Harvey: Harvey's New Eyes nvdec playable 2021-01-26 14:36:08
1066 01004F000B716000 Edna & Harvey: The Breakout – Anniversary Edition crash;nvdec ingame 2022-08-01 16:59:56
1067 01002550129F0000 Effie playable 2022-10-27 14:36:39
1205 01003B200E440000 Five Nights at Freddy's: Sister Location playable 2023-10-06 09:00:58
1206 010038200E088000 Flan crash;regression ingame 2021-11-17 07:39:28
1207 01000A0004C50000 FLASHBACK™ nvdec playable 2020-05-14 13:57:29
1208 0100C53004C52000 Flat Heroes gpu ingame playable 2022-07-26 19:37:37 2026-02-27 17:00:00
1209 0100B54012798000 Flatland: Prologue playable 2020-12-11 20:41:12
1210 0100307004B4C000 Flinthook online playable 2021-03-25 20:42:29
1211 010095A004040000 Flip Wars services;ldn-untested ingame 2022-05-02 15:39:18
1395 0100c3c012718000 Grand Theft Auto: III – The Definitive Edition gpu;UE4 ingame 2022-10-31 20:13:52
1396 0100182014022000 Grand Theft Auto: Vice City – The Definitive Edition gpu;UE4 ingame 2022-10-31 20:13:52
1397 010065a014024000 Grand Theft Auto: San Andreas – The Definitive Edition gpu;UE4 ingame 2022-10-31 20:13:52
1398 0100EB500D92E000 GROOVE COASTER WAI WAI PARTY!!!! gpu ingame 2026-05-13 18:32:12
1399 0100822012D76000 HAAK gpu ingame 2023-02-19 14:31:05
1400 01007E100EFA8000 Habroxia playable 2020-06-16 23:04:42
1401 0100535012974000 Hades vulkan playable 2022-10-05 10:45:21
1834 010055200E87E000 Metamorphosis UE4;audout;gpu;nvdec ingame 2021-06-16 16:18:11
1835 0100D4900E82C000 Metro 2033 Redux gpu ingame 2022-11-09 10:53:13
1836 0100F0400E850000 Metro: Last Light Redux slow;nvdec;vulkan-backend-bug ingame 2023-11-01 11:53:52
1837 010019A01E2F2000 Metroid Prime 4: Beyond ingame 2026-05-13 18:32:12
1838 010012101468C000 Metroid Prime™ Remastered gpu;Needs Update;vulkan-backend-bug;opengl-backend-bug ingame 2024-05-07 22:48:15
1839 010093801237C000 Metroid™ Dread playable 2023-11-13 04:02:36
1840 0100A1200F20C000 Midnight Evil playable 2022-10-18 22:55:19
1948 0100C3E00ACAA000 Mutant Football League: Dynasty Edition online-broken playable 2022-08-05 17:01:51
1949 01004BE004A86000 Mutant Mudds Collection playable 2022-08-05 17:11:38
1950 0100E6B00DEA4000 Mutant Year Zero: Road to Eden - Deluxe Edition nvdec;UE4 playable 2022-09-10 13:31:10
1951 010037501F864000 Mute Crimson DX ingame 2026-05-13 18:32:12
1952 0100161009E5C000 MX Nitro: Unleashed playable 2022-09-27 22:34:33
1953 0100218011E7E000 MX vs ATV All Out nvdec;UE4;vulkan-backend-bug playable 2022-10-25 19:51:46
1954 0100D940063A0000 MXGP3 - The Official Motocross Videogame UE4;gpu;nvdec ingame 2020-12-16 14:00:20
2272 010086F0064CE000 Poi: Explorer Edition nvdec playable 2021-01-21 19:32:00
2273 0100EB6012FD2000 Poison Control playable 2021-05-16 14:01:54
2274 010072400E04A000 Pokémon Café ReMix playable 2021-08-17 20:00:04
2275 01005B7008C52800 Pokémon Champions Needs Update;services;online-broke menus 2026-05-13 18:32:12
2276 010008c01e742000 Pokémon Friends crash;services menus 2025-07-24 13:32:00
2277 01003D200BAA2000 Pokémon Mystery Dungeon™: Rescue Team DX mac-bug playable 2024-01-21 00:16:32
2278 01008DB008C2C000 Pokémon Shield + Pokémon Shield Expansion Pass deadlock;crash;online-broken;ldn-works;LAN ingame 2024-08-12 07:20:22
2280 01009AD008C4C000 Pokémon: Let's Go, Pikachu! demo slow;demo playable 2023-11-26 11:23:20
2281 0100000011D90000 Pokémon™ Brilliant Diamond gpu;ldn-works ingame 2024-08-28 13:26:35
2282 010018E011D92000 Pokémon™ Shining Pearl gpu;ldn-works ingame 2024-08-28 13:26:35
2283 100554023408000 Pokémon FireRed Version crashes nothing 2026-05-13 18:32:12
2284 010034D02340E000 Pokémon LeafGreen Version crashes nothing 2026-05-13 18:32:12
2285 010015F008C54000 Pokémon™ HOME Needs Update;crash;services menus 2020-12-06 06:01:51
2286 01001F5010DFA000 Pokémon™ Legends: Arceus gpu;Needs Update;ldn-works ingame 2024-09-19 10:02:02
2287 0100F43008C44000 Pokémon™ Legends: Z-A gpu;crash;ldn-works ingame 2025-11-16 00:30:00
2873 0100277011F1A000 Super Mario Bros.™ 35 online-broken menus 2022-08-07 16:27:25
2874 010015100B514000 Super Mario Bros.™ Wonder amd-vendor-bug playable 2024-09-06 13:21:21
2875 01009B90006DC000 Super Mario Maker™ 2 online-broken;ldn-broken playable 2024-08-25 11:05:19
2876 0100000000010000 Super Mario Odyssey™ nvdec;intel-vendor-bug;mac-bug nvdec;intel-vendor-bug;mac-bug;amd-vendor-bug playable 2024-08-25 01:32:34 2026-05-13 18:32:12
2877 010036B0034E4000 Super Mario Party™ gpu;Needs Update;ldn-works ingame 2024-06-21 05:10:16
2878 0100965017338000 Super Mario Party Jamboree mac-bug;gpu ingame 2025-02-17 02:09:20
2879 0100BC0018138000 Super Mario RPG™ gpu;audio;nvdec ingame 2024-06-19 17:43:42
3170 0100E2E00CB14000 Tokyo School Life playable 2022-09-16 20:25:54
3171 010024601BB16000 Tomb Raider I-III Remastered Starring Lara Croft gpu;opengl ingame 2024-09-27 12:32:04
3172 0100D7F01E49C000 Tomba! Special Edition services-horizon nothing 2024-09-15 21:59:54
3173 010051F0207B2000 Tomodachi Life: Living the Dream amd-vendor-bug;gpu;intel-vendor-bug;ldn-broken ingame 2026-05-13 18:32:12
3174 0100CA502552A000 Tomodachi Life: Living the Dream – Welcome Edtion amd-vendor-bug;demo playable 2026-05-13 18:32:12
3175 0100D400100F8000 Tonight We Riot playable 2021-02-26 15:55:09
3176 0100CC00102B4000 Tony Hawk's™ Pro Skater™ 1 + 2 gpu;Needs Update ingame 2024-09-24 08:18:14
3177 010093F00E818000 Tools Up! crash ingame 2020-07-21 12:58:17

View File

@@ -51,6 +51,7 @@ namespace Ryujinx.Common.Logging
ServiceNgct,
ServiceNifm,
ServiceNim,
ServiceNotification,
ServiceNs,
ServiceNsd,
ServiceNtc,

View File

@@ -12,6 +12,7 @@ namespace Ryujinx.Common.Logging
Error,
Guest,
AccessLog,
NetLog,
Notice,
Trace,
}

View File

@@ -119,6 +119,7 @@ namespace Ryujinx.Common.Logging
public static Log? Error { get; private set; }
public static Log? Guest { get; private set; }
public static Log? AccessLog { get; private set; }
public static Log? NetLog { get; private set; }
public static Log? Stub { get; private set; }
public static Log? Trace { get; private set; }
public static Log Notice { get; } // Always enabled
@@ -247,6 +248,7 @@ namespace Ryujinx.Common.Logging
case LogLevel.Error : Error = enabled ? new Log(LogLevel.Error) : null; break;
case LogLevel.Guest : Guest = enabled ? new Log(LogLevel.Guest) : null; break;
case LogLevel.AccessLog : AccessLog = enabled ? new Log(LogLevel.AccessLog) : null; break;
case LogLevel.NetLog : NetLog = enabled ? new Log(LogLevel.NetLog) : null; break;
case LogLevel.Stub : Stub = enabled ? new Log(LogLevel.Stub) : null; break;
case LogLevel.Trace : Trace = enabled ? new Log(LogLevel.Trace) : null; break;
case LogLevel.Notice : break;

View File

@@ -59,6 +59,7 @@ namespace Ryujinx.Common
//Mario Franchise
"010021d00812a000", // Arcade Archives VS. SUPER MARIO BROS.
"01007fe0221d8000", // Hello, Mario!
"01006d0017f7a000", // Mario & Luigi: Brothership
"010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020
"010067300059a000", // Mario + Rabbids: Kingdom Battle
@@ -70,6 +71,9 @@ namespace Ryujinx.Common
"0100bde00862a000", // Mario Tennis Aces
"0100b99019412000", // Mario vs. Donkey Kong
"010049900f546000", // Super Mario 3D All-Stars
"010049900f546001", // Super Mario 3D All-Stars | Super Mario 64
"010049900f546002", // Super Mario 3D All-Stars | Super Mario Sunshine
"010049900f546003", // Super Mario 3D All-Stars | Super Mario Galaxy
"010028600ebda000", // Super Mario 3D World + Bowser's Fury
"010049900F546001", // Super Mario 64
"0100ea80032ea000", // Super Mario Bros. U Deluxe
@@ -107,6 +111,11 @@ namespace Ryujinx.Common
"0100187003a36000", // Pokémon: Let's Go Eevee!
"010003f003a34000", // Pokémon: Let's Go Pikachu!
"0100f43008c44000", // Pokémon Legends: Z-A
"0100554023408000", // Pokémon FireRed Version (EN)
"01006fa0233f8000", // Pokémon FireRed Version (JP)
"0100fd6023430000", // Pokémon LeafGreen Version (DE)
"0100f1e0233fa000", // Pokémon LeafGreen Version (JP)
"01005b7008c52000", // Pokémon Champions
//Splatoon Franchise
"0100f8f0000a2000", // Splatoon 2 (EU)
@@ -116,13 +125,14 @@ namespace Ryujinx.Common
"0100ba0018500000", // Splatoon 3: Splatfest World Premiere
//NSO Membership games
"0100d870045b6000", // NES - Nintendo Switch Online
"01008d300c50c000", // SNES - Nintendo Switch Online
"0100c62011050000", // GB - Nintendo Switch Online
"010012f017576000", // GBA - Nintendo Switch Online
"0100c9a00ece6000", // N64 - Nintendo Switch Online
"0100e0601c632000", // N64 - Nintendo Switch Online 18+
"0100d870045b6000", // NES - Nintendo Switch Online
"0100b3c014bda000", // SEGA Genesis - Nintendo Switch Online
"01008d300c50c000", // SNES - Nintendo Switch Online
"0100bfc01d976000", // Virtual Boy - Nintendo Switch Online
"0100ccf019c8c000", // F-ZERO 99
"0100ad9012510000", // PAC-MAN 99
"010040600c5ce000", // Tetris 99
@@ -141,12 +151,17 @@ namespace Ryujinx.Common
"0100704000B3A000", // Snipperclips
"01006a800016e000", // Super Smash Bros. Ultimate
"0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore
"0100ca502552a000", // Tomodachi Life: Living the Dream - Welcome Edition
"010051f0207b2000", // Tomodachi Life: Living the Dream
//Bayonetta Franchise
"010076f0049a2000", // Bayonetta
"01007960049a0000", // Bayonetta 2
"01004a4010fea000", // Bayonetta 3
"0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon
// Famicom Detective Club Franchise
"010054601d54c000", // Emio - The Smiling Man: Famicom Detective Series (DEMO)
//Persona Franchise
"0100dcd01525a000", // Persona 3 Portable
@@ -171,7 +186,9 @@ namespace Ryujinx.Common
"0100453019aa8000", // Xenoblade Chronicles: X Definitive Edition
//Misc Games
"01003670066de000", // 36 Fragments of Midnight
"010056e00853a000", // A Hat in Time
"0100c9f00aaee000", // Ascendence
"0100fd1014726000", // Baldurs Gate: Dark Alliance
"01008c2019598000", // Bluey: The Video Game
"010096f00ff22000", // Borderlands 2: Game of the Year Edition
@@ -185,8 +202,10 @@ namespace Ryujinx.Common
"010027400cdc6000", // Divinity Original 2 - Definitive Edition
"01008c8012920000", // Dying Light Platinum Edition
"0100d11013e6a000", // Eschatos
"01000490067ae000", // Frederic 2: Evil Strikes Back
"01001cc01b2d4000", // Goat Simulator 3
"01003620068ea000", // Hand of Fate 2
"01007ac00e012000", // HEXAGRAVITY
"0100f7e00c70e000", // Hogwarts Legacy
"010013c00e930000", // Hollow Knight: Silksong
"010085500130a000", // Lego City: Undercover
@@ -196,6 +215,7 @@ namespace Ryujinx.Common
"0100853015e86000", // No Man's Sky
"0100f85014ed0000", // No More Heroes
"0100463014ed4000", // No More Heroes 2
"0100f7d00a1bc000", // NO THING
"0100e570094e8000", // Owlboy
"01007bb017812000", // Portal
"0100abd01785c000", // Portal 2
@@ -204,11 +224,14 @@ namespace Ryujinx.Common
"01008e200c5c2000", // Muse Dash
"01005ff002e2a000", // Rayman Legends
"01007820196a6000", // Red Dead Redemption
"01007a800d520000", // REFUNCT
"0100e8300a67a000", // Risk
"01002f7013224000", // Rune Factory 5
"01008d100d43e000", // Saints Row IV
"0100de600beee000", // Saints Row: The Third - The Full Package
"01001180021fa000", // Shovel Knight: Specter of Torment
"010079f00671c000", // Sparkle 2: Evo
"010077b00e046000", // Spyro: Reignited Trilogy
"0100e1D01eb2e000", // Squeakross: Home Squeak Home
"0100e65002bb8000", // Stardew Valley
"0100d7a01b7a2000", // Star Wars: Bounty Hunter

View File

@@ -551,7 +551,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
level,
x,
width,
format.PixelFormat,
(InternalFormat) format.PixelFormat,
mipSize,
data);
}
@@ -579,7 +579,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
layer,
width,
1,
format.PixelFormat,
(InternalFormat) format.PixelFormat,
mipSize,
data);
}
@@ -609,7 +609,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
y,
width,
height,
format.PixelFormat,
(InternalFormat) format.PixelFormat,
mipSize,
data);
}
@@ -643,7 +643,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
width,
height,
1,
format.PixelFormat,
(InternalFormat) format.PixelFormat,
mipSize,
data);
}
@@ -675,7 +675,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
y,
width,
height,
format.PixelFormat,
(InternalFormat) format.PixelFormat,
mipSize,
data);
}
@@ -744,7 +744,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
level,
0,
width,
format.PixelFormat,
(InternalFormat) format.PixelFormat,
mipSize,
data);
}
@@ -773,7 +773,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
0,
width,
height,
format.PixelFormat,
(InternalFormat) format.PixelFormat,
mipSize,
data);
}
@@ -807,7 +807,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
width,
height,
depth,
format.PixelFormat,
(InternalFormat) format.PixelFormat,
mipSize,
data);
}
@@ -843,7 +843,7 @@ namespace Ryujinx.Graphics.OpenGL.Image
0,
width,
height,
format.PixelFormat,
(InternalFormat) format.PixelFormat,
mipSize / 6,
data + faceOffset);
}

View File

@@ -27,7 +27,7 @@ namespace Ryujinx.Graphics.OpenGL
public void Map(BufferHandle handle, int size)
{
GL.BindBuffer(BufferTarget.CopyWriteBuffer, handle);
nint ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, size, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit);
nint ptr = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, size, MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapPersistentBit);
_maps[handle] = ptr;
}
@@ -75,7 +75,7 @@ namespace Ryujinx.Graphics.OpenGL
GL.BindBuffer(BufferTarget.CopyWriteBuffer, _copyBufferHandle);
GL.BufferStorage(BufferTarget.CopyWriteBuffer, requiredSize, nint.Zero, BufferStorageFlags.MapReadBit | BufferStorageFlags.MapPersistentBit);
_bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, requiredSize, BufferAccessMask.MapReadBit | BufferAccessMask.MapPersistentBit);
_bufferMap = GL.MapBufferRange(BufferTarget.CopyWriteBuffer, nint.Zero, requiredSize, MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapPersistentBit);
}
}

View File

@@ -924,8 +924,8 @@ namespace Ryujinx.Graphics.OpenGL
GL.Disable(EnableCap.CullFace);
return;
}
GL.CullFace(face.Convert());
GL.CullFace((TriangleFace) face.Convert());
GL.Enable(EnableCap.CullFace);
}
@@ -1085,12 +1085,12 @@ namespace Ryujinx.Graphics.OpenGL
{
if (frontMode == backMode)
{
GL.PolygonMode(MaterialFace.FrontAndBack, frontMode.Convert());
GL.PolygonMode((TriangleFace) MaterialFace.FrontAndBack, frontMode.Convert());
}
else
{
GL.PolygonMode(MaterialFace.Front, frontMode.Convert());
GL.PolygonMode(MaterialFace.Back, backMode.Convert());
GL.PolygonMode((TriangleFace) MaterialFace.Front, frontMode.Convert());
GL.PolygonMode((TriangleFace) MaterialFace.Back, backMode.Convert());
}
}

View File

@@ -59,7 +59,7 @@ namespace Ryujinx.Graphics.OpenGL
GL.CompileShader(shaderHandle);
break;
case TargetLanguage.Spirv:
GL.ShaderBinary(1, ref shaderHandle, (BinaryFormat)All.ShaderBinaryFormatSpirVArb, shader.BinaryCode, shader.BinaryCode.Length);
GL.ShaderBinary(1, ref shaderHandle, ShaderBinaryFormat.ShaderBinaryFormatSpirV, shader.BinaryCode, shader.BinaryCode.Length);
GL.SpecializeShader(shaderHandle, "main", 0, (int[])null, (int[])null);
break;
}

View File

@@ -32,7 +32,7 @@ namespace Ryujinx.Graphics.OpenGL.Queries
GL.BufferStorage(BufferTarget.QueryBuffer, sizeof(long), (nint)(&defaultValue), BufferStorageFlags.MapReadBit | BufferStorageFlags.MapWriteBit | BufferStorageFlags.MapPersistentBit);
}
_bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, nint.Zero, sizeof(long), BufferAccessMask.MapReadBit | BufferAccessMask.MapWriteBit | BufferAccessMask.MapPersistentBit);
_bufferMap = GL.MapBufferRange(BufferTarget.QueryBuffer, nint.Zero, sizeof(long), MapBufferAccessMask.MapReadBit | MapBufferAccessMask.MapWriteBit | MapBufferAccessMask.MapPersistentBit);
}
public void Reset()

View File

@@ -176,9 +176,7 @@ namespace Ryujinx.Graphics.Vulkan
}
}
}
// This can somehow become -1.
// Logger.Info?.PrintMsg(LogClass.Gpu, $"_referenceCount: {_referenceCount}");
Debug.Assert(_referenceCount >= 0);
}

View File

@@ -44,6 +44,22 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
return ResultCode.Success;
}
[CommandCmif(1)]
// PushOutData(object<nn::am::service::IStorage>)
public ResultCode PushOutData(ServiceCtx context)
{
IStorage appletData = GetObject<IStorage>(context, 0);
if (appletData == null || appletData.Data.Length == 0) // is this necessary?
{
return ResultCode.NullObject;
}
_appletStandalone.InputData.Enqueue(appletData.Data);
return ResultCode.Success;
}
[CommandCmif(11)]
// GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo

View File

@@ -1,7 +1,9 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Caps.Types;
using SkiaSharp;
using System;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -9,16 +11,20 @@ using System.Security.Cryptography;
namespace Ryujinx.HLE.HOS.Services.Caps
{
class CaptureManager
internal class CaptureManager
{
private readonly string _sdCardPath;
public CaptureManager(Switch device)
{
_ = device;
}
private readonly string _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath();
private uint _shimLibraryVersion;
public CaptureManager(Switch device)
{
_sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath();
}
private const int ScreenshotWidth = 1280;
private const int ScreenshotHeight = 720;
private const int ScreenshotBytesPerPixel = 4;
private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * ScreenshotBytesPerPixel; // 0x384000
public ResultCode SetShimLibraryVersion(ServiceCtx context)
{
@@ -53,84 +59,94 @@ namespace Ryujinx.HLE.HOS.Services.Caps
return resultCode;
}
public ResultCode SaveScreenShot(byte[] screenshotData, ulong appletResourceUserId, ulong titleId, out ApplicationAlbumEntry applicationAlbumEntry)
public ResultCode SaveScreenShot(
byte[] screenshotData,
ulong appletResourceUserId,
ulong titleId,
out ApplicationAlbumEntry applicationAlbumEntry)
{
Logger.Stub?.PrintStub(LogClass.ServiceCaps, new
{
appletResourceUserId,
titleId,
screenshotDataLength = screenshotData?.Length ?? 0,
});
applicationAlbumEntry = default;
if (screenshotData.Length == 0)
if (screenshotData == null || screenshotData.Length == 0)
{
return ResultCode.NullInputBuffer;
}
/*
// NOTE: On our current implementation, appletResourceUserId starts at 0, disable it for now.
if (appletResourceUserId == 0)
if (screenshotData.Length < ScreenshotDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid screenshot buffer size 0x{screenshotData.Length:X}; expected at least 0x{ScreenshotDataSize:X}.");
return ResultCode.NullInputBuffer;
}
DateTime currentDateTime = DateTime.Now;
applicationAlbumEntry = new ApplicationAlbumEntry()
{
Size = (ulong)Unsafe.SizeOf<ApplicationAlbumEntry>(),
TitleId = titleId,
AlbumFileDateTime = new AlbumFileDateTime()
{
Year = (ushort)currentDateTime.Year,
Month = (byte)currentDateTime.Month,
Day = (byte)currentDateTime.Day,
Hour = (byte)currentDateTime.Hour,
Minute = (byte)currentDateTime.Minute,
Second = (byte)currentDateTime.Second,
UniqueId = 0,
},
AlbumStorage = AlbumStorage.Sd,
ContentType = ContentType.Screenshot,
Padding = new Array5<byte>(),
Unknown0x1f = 1,
};
// NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead.
string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20];
string folderPath = Path.Combine(
_sdCardPath,
"Nintendo",
"Album",
currentDateTime.Year.ToString("0000", CultureInfo.InvariantCulture),
currentDateTime.Month.ToString("00", CultureInfo.InvariantCulture),
currentDateTime.Day.ToString("00", CultureInfo.InvariantCulture));
string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
_ = Directory.CreateDirectory(folderPath);
while (File.Exists(filePath))
{
applicationAlbumEntry.AlbumFileDateTime.UniqueId++;
filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
}
using SKBitmap bitmap = new(new SKImageInfo(ScreenshotWidth, ScreenshotHeight, SKColorType.Rgba8888));
IntPtr pixels = bitmap.GetPixels();
if (pixels == IntPtr.Zero)
{
return ResultCode.InvalidArgument;
}
*/
/*
// Doesn't occur in our case.
if (applicationAlbumEntry == null)
{
return ResultCode.NullOutputBuffer;
}
*/
Marshal.Copy(screenshotData, 0, pixels, ScreenshotDataSize);
if (screenshotData.Length >= 0x384000)
{
DateTime currentDateTime = DateTime.Now;
using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
using FileStream file = File.OpenWrite(filePath);
data.SaveTo(file);
applicationAlbumEntry = new ApplicationAlbumEntry()
{
Size = (ulong)Unsafe.SizeOf<ApplicationAlbumEntry>(),
TitleId = titleId,
AlbumFileDateTime = new AlbumFileDateTime()
{
Year = (ushort)currentDateTime.Year,
Month = (byte)currentDateTime.Month,
Day = (byte)currentDateTime.Day,
Hour = (byte)currentDateTime.Hour,
Minute = (byte)currentDateTime.Minute,
Second = (byte)currentDateTime.Second,
UniqueId = 0,
},
AlbumStorage = AlbumStorage.Sd,
ContentType = ContentType.Screenshot,
Padding = new Array5<byte>(),
Unknown0x1f = 1,
};
// NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead.
string hash = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId)))[..0x20];
string folderPath = Path.Combine(_sdCardPath, "Nintendo", "Album", currentDateTime.Year.ToString("00"), currentDateTime.Month.ToString("00"), currentDateTime.Day.ToString("00"));
string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
// TODO: Handle that using the FS service implementation and return the right error code instead of throwing exceptions.
Directory.CreateDirectory(folderPath);
while (File.Exists(filePath))
{
applicationAlbumEntry.AlbumFileDateTime.UniqueId++;
filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
}
// NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data.
using SKBitmap bitmap = new(new SKImageInfo(1280, 720, SKColorType.Rgba8888, SKAlphaType.Premul));
int dataLen = screenshotData.Length > bitmap.ByteCount ? bitmap.ByteCount : screenshotData.Length;
Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), dataLen);
using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
using FileStream file = File.OpenWrite(filePath);
data.SaveTo(file);
return ResultCode.Success;
}
return ResultCode.NullInputBuffer;
return ResultCode.Success;
}
private string GenerateFilePath(string folderPath, ApplicationAlbumEntry applicationAlbumEntry, DateTime currentDateTime, string hash)

View File

@@ -1,13 +1,19 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Caps.Types;
namespace Ryujinx.HLE.HOS.Services.Caps
{
[Service("caps:su")] // 6.0.0+
class IScreenShotApplicationService : IpcService
internal class IScreenShotApplicationService : IpcService
{
public IScreenShotApplicationService(ServiceCtx context) { }
private const ulong ScreenshotDataSize = 0x384000;
private const ulong ApplicationDataSize = 0x404;
public IScreenShotApplicationService(ServiceCtx context)
{
_ = context;
}
[CommandCmif(32)] // 7.0.0+
// SetShimLibraryVersion(pid, u64, nn::applet::AppletResourceUserId)
public ResultCode SetShimLibraryVersion(ServiceCtx context)
@@ -33,6 +39,15 @@ namespace Ryujinx.HLE.HOS.Services.Caps
ulong screenshotDataPosition = context.Request.SendBuff[0].Position;
ulong screenshotDataSize = context.Request.SendBuff[0].Size;
if (screenshotDataSize < ScreenshotDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}.");
return ResultCode.NullInputBuffer;
}
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);
@@ -60,6 +75,24 @@ namespace Ryujinx.HLE.HOS.Services.Caps
ulong screenshotDataPosition = context.Request.SendBuff[1].Position;
ulong screenshotDataSize = context.Request.SendBuff[1].Size;
if (applicationDataSize != ApplicationDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid ApplicationData size 0x{applicationDataSize:X}; expected 0x{ApplicationDataSize:X}.");
return ResultCode.InvalidArgument;
}
if (screenshotDataSize < ScreenshotDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}.");
return ResultCode.NullInputBuffer;
}
// TODO: Parse the application data: At 0x00 it's UserData (Size of 0x400), at 0x404 it's a uint UserDataSize (Always empty for now).
_ = context.Memory.GetSpan(applicationDataPosition, (int)applicationDataSize).ToArray();
@@ -88,6 +121,23 @@ namespace Ryujinx.HLE.HOS.Services.Caps
ulong screenshotDataPosition = context.Request.SendBuff[1].Position;
ulong screenshotDataSize = context.Request.SendBuff[1].Size;
if (userIdListSize != 0x88)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid UserIdList size 0x{userIdListSize:X}; expected 0x88.");
return ResultCode.InvalidArgument;
}
if (screenshotDataSize < ScreenshotDataSize)
{
Logger.Warning?.PrintMsg(
LogClass.ServiceCaps,
$"Invalid screenshot buffer size 0x{screenshotDataSize:X}; expected at least 0x{ScreenshotDataSize:X}.");
return ResultCode.NullInputBuffer;
}
// TODO: Parse the UserIdList.
_ = context.Memory.GetSpan(userIdListPosition, (int)userIdListSize).ToArray();

View File

@@ -5,7 +5,6 @@ using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using Ryujinx.Cpu;
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
@@ -15,7 +14,6 @@ using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.Horizon.Common;
using Ryujinx.Memory;
using System;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.NetworkInformation;
@@ -68,10 +66,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (localCommunicationId == localCommunicationIdChecked)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"CheckLocalCommumicationIdPermission: Checked!");
return true;
}
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"CheckLocalCommumicationIdPermission: Check failed!");
return false;
}
@@ -82,7 +81,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_nifmResultCode != ResultCode.Success)
{
context.ResponseData.Write((int)NetworkState.Error);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetState: _nifmResultCode = {_nifmResultCode.ToString()}");
return ResultCode.Success;
}
@@ -114,12 +113,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfo: _nifmResultCode = {_nifmResultCode.ToString()}");
return _nifmResultCode;
}
ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
if (resultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetState: resultCode = {resultCode.ToString()}");
return resultCode;
}
@@ -135,18 +136,22 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_state == NetworkState.StationConnected)
{
networkInfo = _station.NetworkInfo;
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: _station");
}
else if (_state == NetworkState.AccessPointCreated)
{
networkInfo = _accessPoint.NetworkInfo;
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: _accessPoint");
}
else
{
networkInfo = new NetworkInfo();
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"GetNetworkInfoImpl: Invalid state!");
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfoImpl: networkInfo = {networkInfo}");
return ResultCode.InvalidState;
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"GetNetworkInfoImpl: networkInfo = {networkInfo}");
return ResultCode.Success;
}
@@ -198,7 +203,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
else
{
Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
Logger.NetLog?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
@@ -206,7 +211,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
else
{
Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP.");
Logger.NetLog?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP.");
context.ResponseData.Write(config.ProxyIp);
context.ResponseData.Write(config.ProxySubnetMask);
@@ -227,7 +232,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
// NOTE: Returns ResultCode.InvalidArgument if _disconnectReason is null, doesn't occur in our case.
context.ResponseData.Write((short)_disconnectReason);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetDisconnectReason: {_disconnectReason}");
return ResultCode.Success;
}
@@ -247,12 +252,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
if (resultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: resultCode = {resultCode}");
return resultCode;
}
@@ -263,7 +270,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
};
context.ResponseData.WriteStruct(securityParameter);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetSecurityParameter: securityParameter = {securityParameter}");
return ResultCode.Success;
}
@@ -273,12 +281,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
if (resultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: resultCode = {resultCode}");
return resultCode;
}
@@ -292,6 +302,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
};
context.ResponseData.WriteStruct(networkConfig);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkConfig: networkConfig = {networkConfig}");
return ResultCode.Success;
}
@@ -322,12 +334,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkInfoLatestUpdate: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
if (resultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"GetNetworkInfoLatestUpdate: resultCode = {resultCode}");
return resultCode;
}
@@ -378,6 +392,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
@@ -400,6 +415,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (scanFilter.Ssid.Length <= 31)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}");
return resultCode;
}
}
@@ -408,11 +424,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (scanFilterFlag > ScanFilterFlag.All)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}");
return resultCode;
}
if (_state - 3 >= NetworkState.AccessPoint)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ScanImpl: Invalid state!");
resultCode = ResultCode.InvalidState;
}
else
@@ -437,7 +455,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
}
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanImpl: resultCode = {resultCode}");
return resultCode;
}
@@ -462,6 +481,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ScanInternal: availableGames = {availableGames}");
return ResultCode.Success;
}
@@ -502,7 +522,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
throw new ArgumentException($"{GetType().FullName}: Protocol value is not 1 or 3!! Protocol value: {protocolValue}");
}
Logger.Stub?.PrintStub(LogClass.ServiceLdn, $"Protocol value: {protocolValue}");
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetProtocol: protocolValue = {protocolValue}");
Logger.Stub?.PrintStub(LogClass.ServiceLdn, new { protocolValue});
return ResultCode.Success;
}
@@ -512,11 +533,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenAccessPoint: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
if (_state != NetworkState.Initialized)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "OpenAccessPoint: Invalid state!");
return ResultCode.InvalidState;
}
@@ -538,6 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CloseAccessPoint: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
@@ -547,6 +571,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
else
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseAccessPoint: Invalid state!");
return ResultCode.InvalidState;
}
@@ -596,11 +621,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId);
if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CreateNetworkImpl: Invalid object!");
return ResultCode.InvalidObject;
}
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
@@ -629,16 +656,22 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
AddressList addressList = MemoryMarshal.Cast<byte, AddressList>(addressListBytes)[0];
_accessPoint.CreateNetworkPrivate(securityConfig, securityParameter, userConfig, networkConfig, addressList);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: Created private network! " +
$"| securityConfig = {securityConfig} | securityParameter = {securityParameter} " +
$"| userConfig = {userConfig} | networkConfig = {networkConfig} | addressList = {addressList}");
}
else
{
_accessPoint.CreateNetwork(securityConfig, userConfig, networkConfig);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CreateNetworkImpl: Created network! " +
$"| securityConfig = {securityConfig} | userConfig = {userConfig} | networkConfig = {networkConfig}");
}
return ResultCode.Success;
}
else
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CreateNetworkImpl: Invalid state!");
return ResultCode.InvalidState;
}
}
@@ -660,6 +693,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DestroyNetworkImpl: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
@@ -676,9 +710,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
CloseAccessPoint();
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DestroyNetworkImpl: Invalid state!");
return ResultCode.InvalidState;
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DestroyNetworkImpl: Invalid argument!");
return ResultCode.InvalidArgument;
}
@@ -695,14 +731,17 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"RejectImpl: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
if (_state != NetworkState.AccessPointCreated)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "RejectImpl: Invalid state!");
return ResultCode.InvalidState; // Must be network host to reject nodes.
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"RejectImpl: disconnectReason = {disconnectReason} | nodeId = {nodeId}");
return NetworkClient.Reject(disconnectReason, nodeId);
}
@@ -714,11 +753,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetAdvertiseData: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
if (bufferSize is 0 or > LdnConst.AdvertiseDataSizeMax)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetAdvertiseData: Invalid argument!");
return ResultCode.InvalidArgument;
}
@@ -727,11 +768,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
byte[] advertiseData = new byte[bufferSize];
context.Memory.Read(bufferPosition, advertiseData);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetAdvertiseData: advertiseData = {advertiseData}");
return _accessPoint.SetAdvertiseData(advertiseData);
}
else
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetAdvertiseData: Invalid state!");
return ResultCode.InvalidState;
}
}
@@ -744,20 +786,24 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetStationAcceptPolicy: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
if (acceptPolicy > AcceptPolicy.WhiteList)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetStationAcceptPolicy: Invalid argument!");
return ResultCode.InvalidArgument;
}
if (_state is NetworkState.AccessPoint or NetworkState.AccessPointCreated)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"SetStationAcceptPolicy: acceptPolicy = {acceptPolicy}");
return _accessPoint.SetStationAcceptPolicy(acceptPolicy);
}
else
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "SetStationAcceptPolicy: Invalid state!");
return ResultCode.InvalidState;
}
}
@@ -768,6 +814,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"AddAcceptFilterEntry: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
@@ -782,6 +829,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ClearAcceptFilter: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
@@ -796,11 +844,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenStation: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
if (_state != NetworkState.Initialized)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "OpenStation: Invalid state!");
return ResultCode.InvalidState;
}
@@ -813,6 +863,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
// NOTE: Calls nifm service and returns related result codes.
// Since we use our own implementation we can return ResultCode.Success.
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"OpenStation: _station = {_station}");
return ResultCode.Success;
}
@@ -823,6 +875,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"CloseStation: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
@@ -832,11 +885,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
else
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseStation: Invalid state!");
return ResultCode.InvalidState;
}
SetState(NetworkState.Initialized);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "CloseStation: Closed.");
return ResultCode.Success;
}
@@ -901,11 +956,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId);
if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ConnectImpl: Invalid object!");
return ResultCode.InvalidObject;
}
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
@@ -925,6 +982,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_state != NetworkState.Station)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "ConnectImpl: Invalid state!");
resultCode = ResultCode.InvalidState;
}
else
@@ -932,10 +990,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (isPrivate)
{
resultCode = _station.ConnectPrivate(securityConfig, securityParameter, userConfig, localCommunicationVersion, optionUnknown, networkConfig);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: Private connection established! " +
$"| securityConfig = {securityConfig} | securityParameter = {securityParameter} | userConfig = {userConfig} " +
$"| localCommunicationVersion = {localCommunicationVersion} | optionUnknown = {optionUnknown} | networkConfig = {networkConfig}");
}
else
{
resultCode = _station.Connect(securityConfig, userConfig, localCommunicationVersion, optionUnknown, networkInfo);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: Connection established! " +
$"| securityConfig = {securityConfig} | userConfig = {userConfig} " +
$"| localCommunicationVersion = {localCommunicationVersion} | optionUnknown = {optionUnknown} | networkConfig = {networkConfig}");
}
}
}
@@ -943,6 +1007,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"ConnectImpl: resultCode = {resultCode}");
return resultCode;
}
@@ -957,6 +1023,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DisconnectImpl: _nifmResultCode = {_nifmResultCode}");
return _nifmResultCode;
}
@@ -970,14 +1037,17 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_disconnectReason = disconnectReason;
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"DisconnectImpl: _disconnectReason = {_disconnectReason}");
return ResultCode.Success;
}
CloseStation();
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DisconnectImpl: Invalid state!");
return ResultCode.InvalidState;
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "DisconnectImpl: Invalid argument!");
return ResultCode.InvalidArgument;
}
@@ -994,6 +1064,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
if (_nifmResultCode != ResultCode.Success)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"Finalize: _disconnectReason = {_disconnectReason}");
return _nifmResultCode;
}
@@ -1010,11 +1081,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_stateChangeEventHandle = 0;
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"Finalize: resultCode = {resultCode}");
return resultCode;
}
private ResultCode FinalizeImpl(bool isCausedBySystem)
{
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, "FinalizeImpl");
DisconnectReason disconnectReason;
switch (_state)
@@ -1138,7 +1211,6 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
NetworkClient.SetGameVersion(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion);
resultCode = ResultCode.Success;
_nifmResultCode = resultCode;
SetState(NetworkState.Initialized);
@@ -1152,6 +1224,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
}
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"InitializeImpl: resultCode = {resultCode}");
return resultCode;
}

View File

@@ -132,7 +132,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
protected override void OnConnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}");
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}");
UpdatePassphraseIfNeeded();
@@ -141,7 +141,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
protected override void OnDisconnected()
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}");
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}");
_passphrase = null;
@@ -174,7 +174,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
protected override void OnError(SocketError error)
{
Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}");
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}");
_error.Set();
}
@@ -428,7 +428,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
}
else
{
Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}.");
Logger.NetLog?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}.");
_hostedProxy.Start();
(_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface();

View File

@@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
@@ -36,10 +37,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (Connected)
{
_parent.SetState(NetworkState.StationConnected);
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,$"NetworkChanged: {NetworkInfo}");
}
else
{
_parent.SetDisconnectReason(e.DisconnectReasonOrDefault(DisconnectReason.DestroyedByUser));
Logger.NetLog?.PrintMsg(LogClass.ServiceLdn,"NetworkChanged: Disconnected (DestroyedByUser)");
}
}
else

View File

@@ -81,8 +81,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii
return ResultCode.Success;
}
public ResultCode UpdateLatest<T>(DatabaseSessionMetadata metadata, IStoredData<T> oldMiiData, SourceFlag flag, IStoredData<T> newMiiData) where T : unmanaged
public ResultCode UpdateLatest<T>(DatabaseSessionMetadata metadata, T oldMiiData, SourceFlag flag, out T newMiiData) where T : unmanaged, IStoredData<T>
{
newMiiData = default;
if (!flag.HasFlag(SourceFlag.Database))
{
return ResultCode.NotFound;
@@ -106,7 +108,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii
newMiiData.SetFromStoreData(storeData);
if (oldMiiData == newMiiData)
if (oldMiiData.Equals(newMiiData))
{
return ResultCode.NotUpdated;
}
@@ -286,6 +288,18 @@ namespace Ryujinx.HLE.HOS.Services.Mii
return result;
}
public ResultCode Append(DatabaseSessionMetadata metadata, CharInfo charInfo)
{
ResultCode result = _miiDatabase.Append(metadata, _utilityImpl, charInfo);
if (result == ResultCode.Success)
{
result = _miiDatabase.SaveDatabase();
}
return result;
}
public ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData)
{
coreData = new CoreData();

View File

@@ -449,6 +449,32 @@ namespace Ryujinx.HLE.HOS.Services.Mii
return ResultCode.Success;
}
public ResultCode Append(DatabaseSessionMetadata metadata, UtilityImpl utilityImpl, CharInfo charInfo)
{
if (!charInfo.IsValid())
{
return ResultCode.InvalidCharInfo;
}
if (charInfo.Type == 1)
{
return ResultCode.InvalidOperationOnSpecialMii;
}
CoreData coreData = new();
coreData.SetFromCharInfo(charInfo);
StoreData storeData;
do
{
storeData = StoreData.BuildFromCoreData(utilityImpl, coreData);
}
while (_database.GetIndexByCreatorId(out _, storeData.CreateId));
return AddOrReplace(metadata, storeData);
}
public ResultCode Delete(DatabaseSessionMetadata metadata, CreateId createId)
{
if (!_database.GetIndexByCreatorId(out int index, createId))

View File

@@ -54,9 +54,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
protected override ResultCode UpdateLatest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo)
{
newCharInfo = default;
return _database.UpdateLatest(_metadata, oldCharInfo, flag, newCharInfo);
return _database.UpdateLatest(_metadata, oldCharInfo, flag, out newCharInfo);
}
protected override ResultCode BuildRandom(Age age, Gender gender, Race race, out CharInfo charInfo)
@@ -113,14 +111,14 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
protected override ResultCode UpdateLatest1(StoreData oldStoreData, SourceFlag flag, out StoreData newStoreData)
{
newStoreData = default;
if (!_isSystem)
{
newStoreData = default;
return ResultCode.PermissionDenied;
}
return _database.UpdateLatest(_metadata, oldStoreData, flag, newStoreData);
return _database.UpdateLatest(_metadata, oldStoreData, flag, out newStoreData);
}
protected override ResultCode FindIndex(CreateId createId, bool isSpecial, out int index)
@@ -262,5 +260,10 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
{
return _database.ConvertCharInfoToCoreData(charInfo, out coreData);
}
protected override ResultCode Append(CharInfo charInfo)
{
return _database.Append(_metadata, charInfo);
}
}
}

View File

@@ -340,6 +340,15 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
return result;
}
[CommandCmif(26)] // 10.2.0+
// Append(nn::mii::CharInfo char_info)
public ResultCode Append(ServiceCtx context)
{
CharInfo charInfo = context.RequestData.ReadStruct<CharInfo>();
return Append(charInfo);
}
private Span<byte> CreateByteSpanFromBuffer(ServiceCtx context, IpcBuffDesc ipcBuff, bool isOutput)
{
byte[] rawData;
@@ -421,5 +430,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.StaticService
protected abstract ResultCode ConvertCoreDataToCharInfo(CoreData coreData, out CharInfo charInfo);
protected abstract ResultCode ConvertCharInfoToCoreData(CharInfo charInfo, out CoreData coreData);
protected abstract ResultCode Append(CharInfo charInfo);
}
}

View File

@@ -0,0 +1,24 @@
namespace Ryujinx.HLE.HOS.Services.Notification
{
[Service("notif:s")] // 9.0.0+
class INotificationServices : IpcService
{
public INotificationServices(ServiceCtx context) { }
[CommandCmif(1000)] // 9.0.0+
// GetNotificationCount() -> nn::notification::server::INotificationSystemEventAccessor
public ResultCode GetNotificationCount(ServiceCtx context)
{
MakeObject(context, new INotificationSystemEventAccessor(context));
return ResultCode.Success;
}
[CommandCmif(1040)] // 9.0.0+
// GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor
public ResultCode GetNotificationSendingNotifier(ServiceCtx context)
{
MakeObject(context, new INotificationSystemEventAccessor(context));
return ResultCode.Success;
}
}
}

View File

@@ -1,8 +1,33 @@
using Ryujinx.Common.Logging;
namespace Ryujinx.HLE.HOS.Services.Notification
{
[Service("notif:a")] // 9.0.0+
class INotificationServicesForApplication : IpcService
{
public INotificationServicesForApplication(ServiceCtx context) { }
// Leaving this here since I can never find it: https://switchbrew.org/wiki/Glue_services
[CommandCmif(520)] // 9.0.0+
// ListAlarmSettings(nn::arp::ApplicationCertificate) -> s32 AlarmSettingsCount
public ResultCode ListAlarmSettings(ServiceCtx context)
{
// TO-DO: Currently just returns 0. Should read in an ApplicationCertificate.
int alarmSettingsCount = 0;
context.ResponseData.Write(alarmSettingsCount);
return ResultCode.Success;
}
[CommandCmif(1000)] // 9.0.0+
// Initialize(PID-descriptor, u64 pid_reserved)
public ResultCode Intialize(ServiceCtx context)
{
ulong pid = context.Request.HandleDesc.PId;
context.RequestData.ReadUInt64(); // pid placeholder, zero
Logger.Stub?.PrintStub(LogClass.ServiceNotification, new { pid });
return ResultCode.Success;
}
}
}

View File

@@ -1,8 +0,0 @@
namespace Ryujinx.HLE.HOS.Services.Notification
{
[Service("notif:s")] // 9.0.0+
class INotificationServicesForSystem : IpcService
{
public INotificationServicesForSystem(ServiceCtx context) { }
}
}

View File

@@ -0,0 +1,32 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Horizon.Common;
using System;
namespace Ryujinx.HLE.HOS.Services.Notification
{
class INotificationSystemEventAccessor : IpcService
{
private readonly KEvent _getNotificationSendingNotifierEvent;
private int _getNotificationSendingNotifierEventHandle;
public INotificationSystemEventAccessor(ServiceCtx context) { }
[CommandCmif(0)] // 9.0.0+
// GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor
public ResultCode GetSystemEvent(ServiceCtx context)
{
if (_getNotificationSendingNotifierEventHandle == 0)
{
if (context.Process.HandleTable.GenerateHandle(_getNotificationSendingNotifierEvent.ReadableEvent, out _getNotificationSendingNotifierEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_getNotificationSendingNotifierEventHandle);
return ResultCode.Success;
}
}
}

View File

@@ -28,6 +28,7 @@ namespace Ryujinx.HLE.Loaders.Processes
private ulong _latestPid;
#nullable enable
public ProcessResult? ActiveApplication
{
get
@@ -44,6 +45,7 @@ namespace Ryujinx.HLE.Loaders.Processes
return value;
}
}
#nullable disable
public ProcessLoader(Switch device)
{

View File

@@ -0,0 +1,151 @@
using Ryujinx.HLE.HOS.Services.Hid;
using SDL;
using static SDL.SDL3;
using System;
namespace Ryujinx.Input.SDL3
{
/// <summary>
/// Manages a HID handle of a gamepad to encode and write HD rumble commands for Nin controllers.
/// </summary>
public unsafe class NpadHdRumble : IDisposable
{
private readonly SDL_hid_device* _hidHandle;
private int _globalCount;
private NpadHdRumble(SDL_hid_device* hidHandle)
{
_hidHandle = hidHandle;
}
public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle)
{
ushort vendor = SDL_GetGamepadVendor(gamepadHandle);
if (vendor != 0x057e)
{
return null;
}
ushort product = SDL_GetGamepadProduct(gamepadHandle);
if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e)
{
return null;
}
return new NpadHdRumble(SDL_hid_open(vendor, product, 0));
}
// Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble
private void WriteHdRumble(
int encLeftLowFreq, int encLeftLowAmp,
int encLeftHighFreq, int encLeftHighAmp,
int encRightLowFreq, int encRightLowAmp,
int encRightHighFreq, int encRightHighAmp)
{
byte[] buf = new byte[10];
buf[0] = 0x10;
buf[1] = (byte)((++_globalCount) & 0xF);
buf[2] = (byte)(encLeftHighFreq & 0xFF);
buf[3] = (byte)(encLeftHighAmp + ((encLeftHighFreq >> 8) & 0xFF));
buf[4] = (byte)(encLeftLowFreq + ((encLeftLowAmp >> 8) & 0xFF));
buf[5] = (byte)(encLeftLowAmp & 0xFF);
buf[6] = (byte)(encRightHighFreq & 0xFF);
buf[7] = (byte)(encRightHighAmp + ((encRightHighFreq >> 8) & 0xFF));
buf[8] = (byte)(encRightLowFreq + ((encRightLowAmp >> 8) & 0xFF));
buf[9] = (byte)(encRightLowAmp & 0xFF);
if (_globalCount > 0xF)
{
_globalCount = 0x0;
}
fixed (byte* ptr = buf)
{
SDL_hid_write(_hidHandle, ptr, (nuint)buf.Length);
}
}
private static int EncodeLowFreq(float lowFreq)
{
float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f);
return (int)Math.Round(32 * Math.Log2(lf * 0.1f)) - 0x40;
}
private static int EncodeHighFreq(float highFreq)
{
float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f);
return ((int)Math.Round(32 * Math.Log2(hf * 0.1f)) - 0x60) * 4;
}
private static int EncodeLowAmp(float rawAmp)
{
int encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f)
{
encodedAmp = 1;
}
else if (rawAmp is >= 0.012f and < 0.112f)
{
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
}
else if (rawAmp is >= 0.112f and < 0.225f)
{
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
}
else if (rawAmp is >= 0.225f and <= 1f)
{
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
}
return (int)Math.Floor(encodedAmp / 2.0) + 64;
}
private static int EncodeHighAmp(float rawAmp)
{
int encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f)
{
encodedAmp = 1;
}
else if (rawAmp is >= 0.012f and < 0.112f)
{
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
}
else if (rawAmp is >= 0.112f and < 0.225f)
{
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
}
else if (rawAmp is >= 0.225f and <= 1f)
{
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
}
return encodedAmp * 2;
}
public bool HdRumble(VibrationValue left, VibrationValue right)
{
WriteHdRumble(EncodeLowFreq(left.FrequencyLow),
EncodeLowAmp(left.AmplitudeLow),
EncodeHighFreq(left.FrequencyHigh),
EncodeHighAmp(left.AmplitudeHigh),
EncodeLowFreq(right.FrequencyLow),
EncodeLowAmp(right.AmplitudeLow),
EncodeHighFreq(right.FrequencyHigh),
EncodeHighAmp(right.AmplitudeHigh));
return true;
}
public void Dispose()
{
SDL_hid_close(_hidHandle);
}
}
}

View File

@@ -2,6 +2,7 @@ using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -76,11 +77,14 @@ namespace Ryujinx.Input.SDL3
private SDL_Gamepad* _gamepadHandle;
private NpadHdRumble _hdRumble;
private float _triggerThreshold;
public SDL3Gamepad(SDL_Gamepad* gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
_hdRumble = NpadHdRumble.Create(gamepadHandle);
_buttonsUserMapping = new List<ButtonMappingEntry>(20);
Name = SDL_GetGamepadName(_gamepadHandle);
@@ -165,6 +169,10 @@ namespace Ryujinx.Input.SDL3
protected virtual void Dispose(bool disposing)
{
if (disposing && _hdRumble != null)
{
_hdRumble.Dispose();
}
if (disposing && _gamepadHandle != null)
{
SDL_CloseGamepad(_gamepadHandle);
@@ -184,6 +192,11 @@ namespace Ryujinx.Input.SDL3
_triggerThreshold = triggerThreshold;
}
public bool HDRumble(VibrationValue left, VibrationValue right)
{
return _hdRumble?.HdRumble(left, right) ?? false;
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if ((Features & GamepadFeaturesFlag.Rumble) == 0)

View File

@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -61,6 +62,8 @@ namespace Ryujinx.Input.SDL3
public GamepadFeaturesFlag Features { get; }
private SDL_Gamepad* _gamepadHandle;
private NpadHdRumble _hdRumble;
private enum JoyConType
{
@@ -76,6 +79,7 @@ namespace Ryujinx.Input.SDL3
public SDL3JoyCon(SDL_Gamepad* gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
_hdRumble = NpadHdRumble.Create(gamepadHandle);
_buttonsUserMapping = new List<ButtonMappingEntry>(10);
Name = SDL_GetGamepadName(_gamepadHandle);
@@ -139,6 +143,10 @@ namespace Ryujinx.Input.SDL3
protected virtual void Dispose(bool disposing)
{
if (disposing && _hdRumble != null)
{
_hdRumble.Dispose();
}
if (disposing && _gamepadHandle != null)
{
SDL_CloseGamepad(_gamepadHandle);
@@ -156,6 +164,11 @@ namespace Ryujinx.Input.SDL3
{
}
public bool HDRumble(VibrationValue left, VibrationValue right)
{
return _hdRumble?.HdRumble(left, right) ?? false;
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{

View File

@@ -559,18 +559,29 @@ namespace Ryujinx.Input.HLE
{
VibrationValue leftVibrationValue = dualVibrationValue.Item1;
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
_gamepad?.Rumble(low, high, uint.MaxValue);
if (_gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false)
{
_gamepad?.Rumble(low, high, uint.MaxValue);
}
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
$"L.low.amp={leftVibrationValue.AmplitudeLow}, " +
$"L.high.amp={leftVibrationValue.AmplitudeHigh}, " +
$"L.low.freq={leftVibrationValue.FrequencyLow}, " +
$"L.high.freq={leftVibrationValue.FrequencyHigh}, " +
$"R.low.amp={rightVibrationValue.AmplitudeLow}, " +
$"R.high.amp={rightVibrationValue.AmplitudeHigh} " +
$"--> ({low}, {high})");
$"R.low.freq={rightVibrationValue.FrequencyLow}, " +
$"R.high.freq={rightVibrationValue.FrequencyHigh}");
}
}
}

View File

@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -74,6 +75,16 @@ namespace Ryujinx.Input
public void ClearLed() => SetLed(0);
/// <summary>
/// Starts an HD vibration effect on the gamepad if available.
/// </summary>
/// <param name="left">The vibration data for the left side</param>
/// <param name="right">The vibration data for the right side</param>
bool HDRumble(VibrationValue left, VibrationValue right)
{
return false;
}
/// <summary>
/// Starts a rumble effect on the gamepad.
/// </summary>

View File

@@ -0,0 +1,187 @@
using NUnit.Framework;
using Ryujinx.HLE.HOS.Services.Caps;
using Ryujinx.HLE.HOS.Services.Caps.Types;
using SkiaSharp;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Ryujinx.Tests.HLE
{
public class CaptureManagerTests
{
private const int ScreenshotWidth = 1280;
private const int ScreenshotHeight = 720;
private const int BytesPerPixel = 4;
private const int ScreenshotDataSize = ScreenshotWidth * ScreenshotHeight * BytesPerPixel; // 0x384000
private const int PaddedScreenshotDataSize = ScreenshotWidth * 768 * BytesPerPixel; // 0x3C0000
[Test]
public void SaveScreenShotRejectsBufferSmallerThan720p()
{
using TempSdCard tempSdCard = new();
CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path);
byte[] screenshotData = new byte[ScreenshotDataSize - 1];
ResultCode result = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out _);
Assert.That(result, Is.EqualTo(ResultCode.NullInputBuffer));
Assert.That(Directory.Exists(Path.Combine(tempSdCard.Path, "Nintendo", "Album")), Is.False);
}
[Test]
public void SaveScreenShotAcceptsExact720pBuffer()
{
using TempSdCard tempSdCard = new();
CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path);
byte[] screenshotData = CreateTestPattern(ScreenshotDataSize);
ResultCode result = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out ApplicationAlbumEntry applicationAlbumEntry);
string filePath = GetSingleAlbumFile(tempSdCard.Path);
using SKBitmap bitmap = SKBitmap.Decode(filePath);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(ResultCode.Success));
Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth));
Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight));
Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000));
Assert.That(applicationAlbumEntry.AlbumStorage, Is.EqualTo(AlbumStorage.Sd));
Assert.That(applicationAlbumEntry.ContentType, Is.EqualTo(ContentType.Screenshot));
Assert.That(applicationAlbumEntry.Unknown0x1f, Is.EqualTo(1));
});
}
[Test]
public void SaveScreenShotAcceptsBufferLargerThan720p()
{
using TempSdCard tempSdCard = new();
CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path);
byte[] screenshotData = CreateTestPattern(PaddedScreenshotDataSize);
ResultCode result = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out ApplicationAlbumEntry applicationAlbumEntry);
string filePath = GetSingleAlbumFile(tempSdCard.Path);
using SKBitmap bitmap = SKBitmap.Decode(filePath);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(ResultCode.Success));
Assert.That(bitmap.Width, Is.EqualTo(ScreenshotWidth));
Assert.That(bitmap.Height, Is.EqualTo(ScreenshotHeight));
Assert.That(applicationAlbumEntry.TitleId, Is.EqualTo(0x0100000000001000));
});
}
[Test]
public void SaveScreenShotCreatesUniqueFileNamesForRepeatedSaves()
{
using TempSdCard tempSdCard = new();
CaptureManager captureManager = CreateCaptureManager(tempSdCard.Path);
byte[] screenshotData = CreateTestPattern(ScreenshotDataSize);
ResultCode firstResult = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out _);
ResultCode secondResult = captureManager.SaveScreenShot(
screenshotData,
appletResourceUserId: 0,
titleId: 0x0100000000001000,
out _);
string[] files = Directory.GetFiles(
Path.Combine(tempSdCard.Path, "Nintendo", "Album"),
"*.jpg",
SearchOption.AllDirectories);
Assert.Multiple(() =>
{
Assert.That(firstResult, Is.EqualTo(ResultCode.Success));
Assert.That(secondResult, Is.EqualTo(ResultCode.Success));
Assert.That(files, Has.Length.EqualTo(2));
});
}
private static CaptureManager CreateCaptureManager(string sdCardPath)
{
CaptureManager captureManager = (CaptureManager)RuntimeHelpers.GetUninitializedObject(typeof(CaptureManager));
typeof(CaptureManager)
.GetField("_sdCardPath", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(captureManager, sdCardPath);
return captureManager;
}
private static string GetSingleAlbumFile(string sdCardPath)
{
string albumPath = Path.Combine(sdCardPath, "Nintendo", "Album");
string[] files = Directory.GetFiles(albumPath, "*.jpg", SearchOption.AllDirectories);
Assert.That(files, Has.Length.EqualTo(1));
return files.Single();
}
private static byte[] CreateTestPattern(int size)
{
byte[] data = new byte[size];
int pixelCount = size / BytesPerPixel;
for (int i = 0; i < pixelCount; i++)
{
int x = i % ScreenshotWidth;
int y = i / ScreenshotWidth;
data[(i * 4) + 0] = (byte)(x & 0xff);
data[(i * 4) + 1] = (byte)(y & 0xff);
data[(i * 4) + 2] = 0x80;
data[(i * 4) + 3] = 0xff;
}
return data;
}
private sealed class TempSdCard : IDisposable
{
public string Path { get; } = System.IO.Path.Combine(
TestContext.CurrentContext.WorkDirectory,
"sdcard-" + Guid.NewGuid());
public void Dispose()
{
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
}
}
}

View File

@@ -0,0 +1,122 @@
using System.Reflection;
using NUnit.Framework;
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Services.Mii;
using Ryujinx.HLE.HOS.Services.Mii.StaticService;
using Ryujinx.HLE.HOS.Services.Mii.Types;
namespace Ryujinx.Tests.HLE
{
public class MiiDatabaseTests
{
[Test]
public void UpdateLatestReturnsStoredCharInfo()
{
DatabaseImpl database = new();
StoreData storedData = StoreData.BuildDefault(new UtilityImpl(new TickSource(19200000)), 0);
MiiDatabaseManager databaseManager = GetDatabaseManager(database);
NintendoFigurineDatabase figurineDatabase = new();
figurineDatabase.Format();
figurineDatabase.Add(storedData);
SetFigurineDatabase(databaseManager, figurineDatabase);
TestDatabaseService service = new(database);
CharInfo oldCharInfo = new();
oldCharInfo.SetFromStoreData(storedData);
oldCharInfo.Height--;
ResultCode result = service.UpdateLatestForTest(oldCharInfo, SourceFlag.Database, out CharInfo newCharInfo);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(ResultCode.Success));
Assert.That(newCharInfo.CreateId, Is.EqualTo(oldCharInfo.CreateId));
Assert.That(newCharInfo.Height, Is.EqualTo(storedData.CoreData.Height));
Assert.That(newCharInfo.IsValid(), Is.True);
});
}
[Test]
public void AppendAddsRegularCharInfoToDatabase()
{
DatabaseImpl database = new();
UtilityImpl utilityImpl = new(new TickSource(19200000));
SetUtilityImpl(database, utilityImpl);
MiiDatabaseManager databaseManager = GetDatabaseManager(database);
SetFigurineDatabase(databaseManager, CreateFormattedDatabase());
StoreData defaultStoreData = StoreData.BuildDefault(utilityImpl, 0);
Assert.Multiple(() =>
{
Assert.That(defaultStoreData.CoreData.IsValid(), Is.True);
Assert.That(defaultStoreData.IsValidDataCrc(), Is.True);
Assert.That(defaultStoreData.IsValidDeviceCrc(), Is.True);
Assert.That(defaultStoreData.IsValid(), Is.True);
});
CharInfo charInfo = new();
charInfo.SetFromStoreData(defaultStoreData);
DatabaseSessionMetadata metadata = database.CreateSessionMetadata(new SpecialMiiKeyCode());
ResultCode result = databaseManager.Append(metadata, utilityImpl, charInfo);
int count = databaseManager.GetCount(metadata);
databaseManager.Get(metadata, 0, out StoreData storedData);
CoreData expectedCoreData = new();
expectedCoreData.SetFromCharInfo(charInfo);
Assert.Multiple(() =>
{
Assert.That(result, Is.EqualTo(ResultCode.Success));
Assert.That(count, Is.EqualTo(1));
Assert.That(storedData.IsValid(), Is.True);
Assert.That(storedData.CreateId, Is.Not.EqualTo(charInfo.CreateId));
Assert.That(storedData.CoreData, Is.EqualTo(expectedCoreData));
});
}
private sealed class TestDatabaseService(DatabaseImpl database) : DatabaseServiceImpl(database, true, new SpecialMiiKeyCode())
{
public ResultCode UpdateLatestForTest(CharInfo oldCharInfo, SourceFlag flag, out CharInfo newCharInfo)
{
return UpdateLatest(oldCharInfo, flag, out newCharInfo);
}
}
private static MiiDatabaseManager GetDatabaseManager(DatabaseImpl database)
{
return (MiiDatabaseManager)typeof(DatabaseImpl)
.GetField("_miiDatabase", BindingFlags.Instance | BindingFlags.NonPublic)
.GetValue(database);
}
private static void SetFigurineDatabase(MiiDatabaseManager databaseManager, NintendoFigurineDatabase figurineDatabase)
{
typeof(MiiDatabaseManager)
.GetField("_database", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(databaseManager, figurineDatabase);
}
private static NintendoFigurineDatabase CreateFormattedDatabase()
{
NintendoFigurineDatabase figurineDatabase = new();
figurineDatabase.Format();
return figurineDatabase;
}
private static void SetUtilityImpl(DatabaseImpl database, UtilityImpl utilityImpl)
{
typeof(DatabaseImpl)
.GetField("_utilityImpl", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(database, utilityImpl);
}
}
}

View File

@@ -254,6 +254,7 @@ namespace Ryujinx.Headless
Logger.SetEnable(LogLevel.Trace, option.LoggingEnableTrace);
Logger.SetEnable(LogLevel.Guest, !option.LoggingDisableGuest);
Logger.SetEnable(LogLevel.AccessLog, option.LoggingEnableFsAccessLog);
Logger.SetEnable(LogLevel.NetLog, option.LoggingEnableFsAccessLog);
if (!option.DisableFileLog)
{

View File

@@ -108,6 +108,9 @@ namespace Ryujinx.Headless
if (NeedsOverride(nameof(LoggingEnableFsAccessLog)))
LoggingEnableFsAccessLog = configurationState.Logger.EnableFsAccessLog;
if (NeedsOverride(nameof(LoggingEnableNetLog)))
LoggingEnableNetLog = configurationState.Logger.EnableNetLog;
if (NeedsOverride(nameof(LoggingGraphicsDebugLevel)))
LoggingGraphicsDebugLevel = configurationState.Logger.GraphicsDebugLevel;
@@ -370,6 +373,9 @@ namespace Ryujinx.Headless
[Option("enable-fs-access-logs", Required = false, Default = false, HelpText = "Enables printing FS access log messages.")]
public bool LoggingEnableFsAccessLog { get; set; }
[Option("enable-net-logs", Required = false, Default = false, HelpText = "Enables printing net log messages.")]
public bool LoggingEnableNetLog { get; set; }
[Option("graphics-debug-level", Required = false, Default = GraphicsDebugLevel.None, HelpText = "Change Graphics API debug log level.")]
public GraphicsDebugLevel LoggingGraphicsDebugLevel { get; set; }

View File

@@ -622,15 +622,15 @@ namespace Ryujinx.Ava.Systems
// If the GPU has no work and is cancelled, we need to handle that as well.
WaitHandle.WaitAny(new[] { _gpuDoneEvent, _gpuCancellationTokenSource.Token.WaitHandle });
_gpuCancellationTokenSource.Dispose();
// Waiting for work to be finished before we dispose.
if (_renderingStarted)
{
// Waiting for work to be finished before we dispose.
Device.Gpu.WaitUntilGpuReady();
}
_gpuDoneEvent.Dispose();
_gpuCancellationTokenSource.Dispose();
DisposeGpu();
AppExit?.Invoke(this, EventArgs.Empty);
@@ -1095,51 +1095,56 @@ namespace Ryujinx.Ava.Systems
Device.Gpu.Renderer.RunLoop(() =>
{
Device.Gpu.SetGpuThread();
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
_renderer.Window.ChangeVSyncMode(Device.VSyncMode);
while (_isActive)
try
{
_ticks += _chrono.ElapsedTicks;
Device.Gpu.SetGpuThread();
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
_chrono.Restart();
_renderer.Window.ChangeVSyncMode(Device.VSyncMode);
if (Device.WaitFifo())
while (_isActive)
{
Device.Statistics.RecordFifoStart();
Device.ProcessFrame();
Device.Statistics.RecordFifoEnd();
}
_ticks += _chrono.ElapsedTicks;
while (Device.ConsumeFrameAvailable())
{
if (!_renderingStarted)
_chrono.Restart();
if (Device.WaitFifo())
{
_renderingStarted = true;
_viewModel.SwitchToRenderer(false);
InitStatus();
Device.Statistics.RecordFifoStart();
Device.ProcessFrame();
Device.Statistics.RecordFifoEnd();
}
Device.PresentFrame(() => (RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
}
while (Device.ConsumeFrameAvailable())
{
if (!_renderingStarted)
{
_renderingStarted = true;
_viewModel.SwitchToRenderer(false);
InitStatus();
}
if (_ticks >= _ticksPerFrame)
{
UpdateStatus();
Device.PresentFrame(() =>
(RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.SwapBuffers());
}
if (_ticks >= _ticksPerFrame)
{
UpdateStatus();
}
}
}
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
finally
{
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands...");
threaded.FlushThreadedCommands();
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!");
// Make sure all commands in the run loop are fully executed before leaving the loop.
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
{
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushing threaded commands...");
threaded.FlushThreadedCommands();
Logger.Info?.PrintMsg(LogClass.Gpu, "Flushed!");
}
_gpuDoneEvent.Set();
}
_gpuDoneEvent.Set();
});
(RendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(true);

View File

@@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 71;
public const int CurrentVersion = 72;
/// <summary>
/// Version of the configuration file format
@@ -113,6 +113,11 @@ namespace Ryujinx.Ava.Systems.Configuration
/// Enables printing FS access log messages
/// </summary>
public bool LoggingEnableFsAccessLog { get; set; }
/// <summary>
/// Enables printing network log messages
/// </summary>
public bool LoggingEnableNetLog { get; set; }
/// <summary>
/// Enables log messages from Avalonia

View File

@@ -1,4 +1,4 @@
using Avalonia.Media;
using Avalonia.Media;
using Gommon;
using Ryujinx.Ava.Systems.Configuration.System;
using Ryujinx.Ava.Systems.Configuration.UI;
@@ -68,6 +68,7 @@ namespace Ryujinx.Ava.Systems.Configuration
Logger.EnableTrace.Value = cff.LoggingEnableTrace;
Logger.EnableGuest.Value = cff.LoggingEnableGuest;
Logger.EnableFsAccessLog.Value = cff.LoggingEnableFsAccessLog;
Logger.EnableNetLog.Value = cff.LoggingEnableNetLog;
Logger.FilteredClasses.Value = cff.LoggingFilteredClasses;
Logger.GraphicsDebugLevel.Value = cff.LoggingGraphicsDebugLevel;

View File

@@ -257,6 +257,11 @@ namespace Ryujinx.Ava.Systems.Configuration
/// Enables printing FS access log messages
/// </summary>
public ReactiveObject<bool> EnableFsAccessLog { get; private set; }
/// <summary>
/// Enables printing network log messages
/// </summary>
public ReactiveObject<bool> EnableNetLog { get; private set; }
/// <summary>
/// Enables log messages from Avalonia
@@ -289,6 +294,7 @@ namespace Ryujinx.Ava.Systems.Configuration
EnableTrace = new ReactiveObject<bool>();
EnableGuest = new ReactiveObject<bool>();
EnableFsAccessLog = new ReactiveObject<bool>();
EnableNetLog = new ReactiveObject<bool>();
EnableAvaloniaLog = new ReactiveObject<bool>();
FilteredClasses = new ReactiveObject<LogClass[]>();
EnableFileLog = new ReactiveObject<bool>();

View File

@@ -47,6 +47,7 @@ namespace Ryujinx.Ava.Systems.Configuration
LoggingEnableTrace = Logger.EnableTrace,
LoggingEnableGuest = Logger.EnableGuest,
LoggingEnableFsAccessLog = Logger.EnableFsAccessLog,
LoggingEnableNetLog = Logger.EnableNetLog,
LoggingEnableAvalonia = Logger.EnableAvaloniaLog,
LoggingFilteredClasses = Logger.FilteredClasses,
LoggingGraphicsDebugLevel = Logger.GraphicsDebugLevel,
@@ -176,6 +177,7 @@ namespace Ryujinx.Ava.Systems.Configuration
Logger.EnableTrace.Value = false;
Logger.EnableGuest.Value = true;
Logger.EnableFsAccessLog.Value = false;
Logger.EnableNetLog.Value = false;
Logger.EnableAvaloniaLog.Value = false;
Logger.FilteredClasses.Value = [];
Logger.GraphicsDebugLevel.Value = GraphicsDebugLevel.None;

View File

@@ -26,6 +26,8 @@ namespace Ryujinx.Ava.Systems.Configuration
(_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue);
ConfigurationState.Instance.Logger.EnableFsAccessLog.Event +=
(_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue);
ConfigurationState.Instance.Logger.EnableNetLog.Event +=
(_, e) => Logger.SetEnable(LogLevel.NetLog, e.NewValue);
ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) =>
{

View File

@@ -1,5 +1,6 @@
using Gommon;
using Humanizer;
using MsgPack;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
@@ -23,24 +24,382 @@ namespace Ryujinx.Ava.Systems.PlayReport
private static FormattedValue SkywardSwordHD_Rupees(SingleValue value)
=> "rupee".ToQuantity(value.Matched.IntValue);
private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static FormattedValue EchoesOfWisdom_Warp(SingleValue value)
{
FormattedValue locations = value.Matched.IntValue switch
{
// Hyrule Field
23 => "Hyrule Field: Kakariko Village",
43 => "Hyrule Field: West of Hyrule Ranch",
45 => "Hyrule Field: North of Hyrule Ranch",
25 => "Hyrule Field: Hyrule Ranch",
26 => "Hyrule Field: West of Hyrule Castle",
48 => "Hyrule Field: Haunted Grove",
24 => "Hyrule Field: Hyrule Castle",
27 => "Hyrule Field: Northern Sanctuary",
28 => "Eastern Hyrule Field: Eastern Temple",
41 => "Eastern Hyrule Field: Dampé Studio",
22 => "Lake Hylia: Great Fairy Shrine",
// Eternal Forest
47 => "Eternal Forest: Entrance",
46 => "Eternal Forest: Great Deku Tree",
752 => "Eternal Forest: Stilled Ancient Ruins (Halfway Point)",
753 => "Eternal Forest: Stilled Ancient Ruins (Null)",
// Suthorn
33 => "Suthorn Prairie: Lueburry's House",
20 => "Suthorn Prairie: Suthorn Village",
21 => "Suthorn Forest: Suthorn Ruins",
// Faron Wetlands
13 => "Faron Wetlands: Entrance",
15 => "Faron Wetlands: Scrubton",
18 => "Faron Wetlands: Blossu's House",
17 => "Faron Wetlands: Heart Lake",
852 => "Faron Wetlands: Stilled Faron Wetlands",
601 => "Faron Wetlands: Faron Temple 3F",
602 => "Faron Wetlands: Faron Temple 2F (Underwater Entrance)",
603 => "Faron Wetlands: Faron Temple 2F (West Entrance)",
604 => "Faron Wetlands: Faron Temple 2F (Cliff Entrance)",
605 => "Faron Wetlands: Faron Temple 1F (Diababa)",
606 => "Faron Wetlands: Faron Temple 1F (Gohma)",
// Jabul Waters
11 => "Jabul Waters: River Zora Village",
9 => "Jabul Waters: Crossflows Plaza",
8 => "Jabul Waters: Seesyde Village",
12 => "Jabul Waters: Sea Zora Village",
10 => "Jabul Waters: Lord Jabu-Jabu's Den",
201 => "Jabul Waters: Jabul Ruins 1F (Entrance)",
202 => "Jabul Waters: Jabul Ruins 1F (Vocavor)",
// Gerudo Desert
40 => "Gerudo Desert: Entrance",
29 => "Gerudo Desert: Oasis",
32 => "Gerudo Desert: Ancestor's Cave Of Rest",
30 => "Gerudo Desert: Gerudo Town",
31 => "Gerudo Desert: Gerudo Sanctum",
351 => "Gerudo Desert: Stilled Gerudo Sanctum",
303 => "Gerudo Desert: Gerudo Sanctum 1F (West Entrance)",
304 => "Gerudo Desert: Gerudo Sanctum 1F (East Entrance)",
301 => "Gerudo Desert: Gerudo Sanctum 2F (The Key)",
302 => "Gerudo Desert: Gerudo Sanctum 2F (The Elephant Room)",
305 => "Gerudo Desert: Gerudo Sanctum 2F (Mogryph)",
// Eldin Volcano
4 => "Eldin Volcano: Eldin Volcano Trail",
44 => "Eldin Volcano: Lava Lake",
3 => "Eldin Volcano: Goron City",
5 => "Eldin Volcano: Rock Roast Volcano",
49 => "Eldin Volcano: Crater Shortcut",
552 => "Eldin Volcano: Stilled Eldin Volcano",
501 => "Eldin Volcano: Eldin Temple 1F",
503 => "Eldin Volcano: Eldin Temple 2F",
502 => "Eldin Volcano: Eldin Temple 3F",
// Hebra Mountain
34 => "Hebra Mountain: Hebra Mountain Passage (1)",
35 => "Hebra Mountain: Sheltered Hot Spring",
36 => "Hebra Mountain: Condé's House",
38 => "Hebra Mountain: Hebra Mountain Passage (2)",
37 => "Hebra Mountain: Hebra Mountain Passage (3)",
39 => "Hebra Mountain: Summit",
652 => "Hebra Mountain: Stilled Holy Mount Lanayru",
801 => "Hebra Mountain: Lanayru Temple 1F",
802 => "Hebra Mountain: Lanayru Temple B2",
803 => "Hebra Mountain: Lanayru Temple B4",
_ => FormattedValue.ForceReset
};
private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value)
=> value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
return locations.Reset
? FormattedValue.ForceReset
: $"Warped to {locations}";
}
private static FormattedValue SuperMario3DAllStars(SingleValue value)
{
// TODO: Is this really necessary?
FormattedValue title = value.Matched.IntValue switch
{
1 => "Super Mario 64",
2 => "Super Mario Sunshine",
3 => "Super Mario Galaxy",
_ => FormattedValue.ForceReset
};
return title.Reset
? FormattedValue.ForceReset
: $"Playing {title}";
}
private static FormattedValue SuperMario3DAllStars_MainMenu(MultiValue value)
{
int albumId = value.Matched[0].IntValue;
int songId = value.Matched[1].IntValue;
string album = value.Matched[0].IntValue switch
{
1 => "Super Mario 64 OST",
2 => "Super Mario Sunshine OST",
3 => "Super Mario Galaxy OST",
_ => "Listening to Super Mario 3D All-Stars"
};
string song = (albumId, songId) switch
{
// Super Mario 64
(1, 0) => "It's a Me, Mario!",
(1, 1) => "Title Theme",
(1, 2) => "Peach's Message",
(1, 3) => "Opening",
(1, 4) => "Super Mario 64 Main Theme",
(1, 5) => "Slider",
(1, 6) => "Inside the Castle Walls",
(1, 7) => "Looping Steps",
(1, 8) => "Dire, Dire Docks",
(1, 9) => "Lethal Lava Land",
(1, 10) => "Snow Mountain",
(1, 11) => "Haunted House",
(1, 12) => "Merry-Go-Round",
(1, 13) => "Cave Dungeon",
(1, 14) => "Piranha Plant's Lullaby",
(1, 15) => "Powerful Mario",
(1, 16) => "Metallic Mario",
(1, 17) => "File Select",
(1, 18) => "Correct Solution",
(1, 19) => "Toad's Message",
(1, 20) => "Power Star",
(1, 21) => "Race Fanfare",
(1, 22) => "Star Catch Fanfare",
(1, 23) => "Game Start",
(1, 24) => "Course Clear",
(1, 25) => "Game Over",
(1, 26) => "Stage Boss",
(1, 27) => "Koopa's Message",
(1, 28) => "Koopa's Road",
(1, 29) => "Koopa's Theme",
(1, 30) => "Koopa Clear",
(1, 31) => "Ultimate Koopa",
(1, 32) => "Ultimate Koopa Clear",
(1, 33) => "Ending Demo",
(1, 34) => "Staff Roll",
(1, 35) => "Piranha Plant's Lullaby - Piano",
// Super Mario Sunshine
(2, 0) => "Isle Delfino",
(2, 1) => "Delfino Airstrip",
(2, 2) => "Bianco Hills",
(2, 3) => "Ricco Harbor",
(2, 4) => "Gelato Beach",
(2, 5) => "Pinna Beach",
(2, 6) => "Pinna Park",
(2, 7) => "Sirena Beach",
(2, 8) => "Hotel Delfino",
(2, 9) => "Casino",
(2, 10) => "Noki Bay",
(2, 11) => "Noki Depths",
(2, 12) => "Pianta Village",
(2, 13) => "Pianta Hot Spring",
(2, 14) => "Pianta Rescue",
(2, 15) => "Pianta Village - Fluff Festival",
(2, 16) => "Underground",
(2, 17) => "Secret Course",
(2, 18) => "Secret Course - Sky and Sea",
(2, 19) => "Corona Mountain",
(2, 20) => "Mid-Boss",
(2, 21) => "Proto Piranha",
(2, 22) => "Phantamanta",
(2, 23) => "Boss Battle",
(2, 24) => "Gooper Blooper Intro",
(2, 25) => "Wiggler Intro",
(2, 26) => "Mecha-Bowser",
(2, 27) => "Bowser",
(2, 28) => "Shadow Mario",
(2, 29) => "Racing Il Piantissimo",
(2, 30) => "Event",
(2, 31) => "Timed Event",
(2, 32) => "Yoshi-Go-Round",
(2, 33) => "Title Screen",
(2, 34) => "Opening Demo",
(2, 35) => "Select Data",
(2, 36) => "Select Scenario",
(2, 37) => "Course Intro",
(2, 38) => "Course Intro - Shadow Mario",
(2, 39) => "A Shine Sprite Appears",
(2, 40) => "Shine!",
(2, 41) => "Race Fanfare",
(2, 42) => "Casino Fanfare",
(2, 43) => "Too Bad!",
(2, 44) => "Game Over",
(2, 45) => "Welcome to Isle Delfino (Movie)",
(2, 46) => "Icky Goop (Movie)",
(2, 47) => "Mario on Trial (Movie)",
(2, 48) => "How to Use FLUDD (Movie)",
(2, 49) => "Shadow Mario Appears (Movie)",
(2, 50) => "The Kidnapping of Princess Peach (Movie)",
(2, 51) => "Mecha-Bowser Rises (Movie)",
(2, 52) => "Meet Bowser Jr. (Movie)",
(2, 53) => "FLUDD Theft (Movie)",
(2, 54) => "Hot Tub Intrusion (Movie)",
(2, 55) => "Epilogue (Movie)",
(2, 56) => "Staff Credits",
(2, 57) => "Have a Relaxing Vacation!",
// Super Mario Galaxy
(3, 0) => "Overture",
(3, 1) => "The Star Festival",
(3, 2) => "Attack of the Airships",
(3, 3) => "Catastrophe",
(3, 4) => "Peach's Castle Stolen",
(3, 5) => "Enter the Galaxy",
(3, 6) => "Egg Planet",
(3, 7) => "Rosaline in the Observatory 1",
(3, 8) => "The Honeyhive",
(3, 9) => "Space Junk Road",
(3, 10) => "Battlerock Galaxy",
(3, 11) => "Beach Bowl Galaxy",
(3, 12) => "Rosalina in the Observatory 2",
(3, 13) => "Enter Bowser Jr.!",
(3, 14) => "Waltz of the Boos",
(3, 15) => "Buoy Base Galaxy",
(3, 16) => "Gusty Garden Galaxy",
(3, 17) => "Rosaline in the Observatory 3",
(3, 18) => "King Bowser",
(3, 19) => "Melty Molten Galaxy",
(3, 20) => "The Galaxy Reactor",
(3, 21) => "Final Battle with Bowser",
(3, 22) => "A New Dawn",
(3, 23) => "Birth",
(3, 24) => "Super Mario Galaxy",
(3, 25) => "Purple Comet",
(3, 26) => "Blue Sky Athletic",
(3, 27) => "Super Mario 2007",
(3, 28) => "File Select",
(3, 29) => "Luma",
(3, 30) => "Gateway Galaxy",
(3, 31) => "Stolen Grand Star",
(3, 32) => "To the Observatory Grounds 1",
(3, 33) => "Observation Dome",
(3, 34) => "Course Select",
(3, 35) => "Dino Piranha",
(3, 36) => "A Chance to Grab a Star!",
(3, 37) => "A Tense Moment",
(3, 38) => "Big Bad Bugaboom",
(3, 39) => "King Kaliente",
(3, 40) => "The Toad Brigade",
(3, 41) => "Airship Armada",
(3, 42) => "Aquatic Race",
(3, 43) => "Space Fantasy",
(3, 44) => "Megaleg",
(3, 45) => "To The Observatory Grounds 2",
(3, 46) => "Space Athletic",
(3, 47) => "Speedy Comet",
(3, 48) => "Beach Bowl Galaxy - Undersea",
(3, 49) => "Interlude",
(3, 50) => "Bowser's Stronghold Appears",
(3, 51) => "The Fiery Stronghold",
(3, 52) => "The Big Staircase",
(3, 53) => "Bowser Appears",
(3, 54) => "Star Ball",
(3, 55) => "The Library",
(3, 56) => "Buoy Base Galaxy - Undersea",
(3, 57) => "Rainbow Mario",
(3, 58) => "Chase the Bunnies",
(3, 59) => "Help!",
(3, 60) => "Major Burrows",
(3, 61) => "Pipe Interior",
(3, 62) => "Cosmic Comet",
(3, 63) => "Drip Drop Galaxy",
(3, 64) => "Kingfin",
(3, 65) => "Boo Race",
(3, 66) => "Ice Mountain",
(3, 67) => "Ice Mario",
(3, 68) => "Lava Path",
(3, 69) => "Fire Mario",
(3, 70) => "Dusty Dune Galaxy",
(3, 71) => "Heavy Metal Mecha-Bowser",
(3, 72) => "A-wa-wa-wa!",
(3, 73) => "Deep Dark Galaxy",
(3, 74) => "Kamella",
(3, 75) => "Star Ball 2",
(3, 76) => "Sad Girl",
(3, 77) => "Flying Mario",
(3, 78) => "Star Child",
(3, 79) => "A Wish",
(3, 80) => "Family",
_ => ""
};
return string.IsNullOrEmpty(song) ? FormattedValue.ForceReset : $"{album} - {song}";
}
private static FormattedValue SuperMarioOdyssey(SingleValue value)
=> value.Matched.LongValue switch
{
// TODO: Needs updated for sub-areas.
2973331007 => "Cap Kingdom: Bonneton",
2661781375 => "Cascade Kingdom: Fossil Falls",
512560049 => "Sand Kingdom: Tostarena",
3079659402 => "Wooded Kingdom: Steam Gardens",
1941286268 => "Lake Kingdom: Lake Lamode",
3098209122 => "Cloud Kingdom: Nimbus Arena",
4088050842 => "Lost Kingdom: Forgotten Isle",
53003352 => "Metro Kingdom: New Donk City",
4265839612 => "Seaside Kingdom: Bubblaine",
3288863344 => "Snow Kingdom: Shiveria",
3180104973 => "Luncheon Kingdom: Mount Volbono",
2284558980 => "Ruined Kingdom: Crumbleden",
3024139598 => "Bowser's Kingdom: Bowser's Castle",
1351608174 => "Moon Kingdom: Honeylune Ridge",
1698750149 => "Dark Side: Rabbit Ridge",
3206301958 => "Darker Side: Culmina Crater",
3963002526 => "Mushroom Kingdom: Peach's Castle",
_ => FormattedValue.ForceReset
};
private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value)
=> value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static FormattedValue SuperMarioWonder(SingleValue value)
{
// TODO: Needs updated for course names.
MessagePackObject messagePackObject = value.Matched.PackedValue;
MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary();
int worldNumber = messagePackObjectDictionary["world_no"].AsInt32();
int courseNumber = 0;
if (messagePackObjectDictionary.TryGetValue("course_no", out MessagePackObject courseNumberVariable))
{
courseNumber = courseNumberVariable.AsInt32();
}
FormattedValue world = worldNumber switch
{
1 => "Pipe-Rock Plateau",
2 => "Petal Isles",
3 => "Fluff-Puff Peaks",
4 => "Shining Falls",
5 => "Sunbaked Desert",
6 => "Fungi Mines",
7 => "Deep Magma Bog",
9 => "Special World",
_ => FormattedValue.ForceReset
};
if (courseNumber == 0)
{
return FormattedValue.ForceReset;
}
return world.Reset
? FormattedValue.ForceReset
: $"{world}: {worldNumber}-{courseNumber}";
}
private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value)
=> value.Matched.StringValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
"Multi-2players" => "Multiplayer: 2 Players",
"Multi-3players" => "Multiplayer: 3 Players",
"Multi-4players" => "Multiplayer: 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
@@ -62,8 +421,9 @@ namespace Ryujinx.Ava.Systems.PlayReport
private static FormattedValue PokemonSV(MultiValue values)
{
string playStatus = values.Matched[0].BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
string region = PokemonSV_Region(values.Matched[1].ToString());
string union = values.Matched[0].BoxedValue is 0 ? "" : " with friends";
string academyName = PokemonSV_AcademyName(values.Application.Title);
FormattedValue locations = values.Matched[1].ToString() switch
{
@@ -89,18 +449,86 @@ namespace Ryujinx.Ava.Systems.PlayReport
"a_w20" => "North Area Three",
"a_w21" => "North Area One",
"a_w22" => "North Area Two",
"a_w23" => "The Great Crater of Paldea",
"a_w23" => "Area Zero: The Great Crater of Paldea",
"a_w24" => "South Paldean Sea",
"a_w25" => "West Paldean Sea",
"a_w26" => "East Paldean Sea",
"a_w27" => "North Paldean Sea",
//TODO DLC Locations
// Naranja / Uva Academy
"a_sch_entrance01" => $"{academyName} Academy: Entrance",
"a_sch_cafe01" => $"{academyName} Academy: Cafeteria",
"a_sch_shop01" => $"{academyName} Academy: School Store",
"a_sch_room01" => $"{academyName} Academy: Home Ec Room",
"a_sch_room02" => $"{academyName} Academy: Art Room",
"a_sch_room03" => $"{academyName} Academy: Biology Lab",
"a_sch_room04" => $"{academyName} Academy: Staff Room",
"a_sch_office01" => $"{academyName} Academy: Director's Office",
"a_sch_office03" => $"{academyName} Academy: Nurse's Office",
"a_sch_ground01" => $"{academyName} Academy: School Yard",
"a_sch_class1a" => $"{academyName} Academy: Classroom 1-A",
"a_sch_class1d" => $"{academyName} Academy: Classroom 1-D",
"a_sch_class2g" => $"{academyName} Academy: Classroom 2-G",
"a_sch_dorm01" => $"{academyName} Academy: Dorm Room (Trainer)",
"a_sch_dorm02" => $"{academyName} Academy: Dorm Room (Nemona)",
"a_sch_dorm03" => $"{academyName} Academy: Dorm Room (Arven)",
"a_sch_dorm04" => $"{academyName} Academy: Dorm Room (Penny)",
// DLC
// Kitakami
"a_su0101" => "Mossui Town",
"a_su0102" => "Loyalty Plaza",
"a_su0103" => "Kitakami Hall",
"a_su0104" => "Oni Mountain",
"a_su0105" => "Infernal Pass",
"a_su0106" => "Crystal Pool",
"a_su0107" => "Wistful Fields",
"a_su0108" => "Mossfell Confluence",
"a_su0109" => "Fellhorn Gorge",
"a_su0110" => "Paradise Barrens",
"a_su0111" => "Timeless Woods",
// Blueberry Academy: School
"a_sch_2_entrance0" => "Blueberry Academy: Entrance",
"a_sch_2_clubroom" => "Blueberry Academy: League Clubroom",
"a_sch_2_class1" => "Blueberry Academy: Classroom 1-4",
"a_sch_2_class2" => "Blueberry Academy: Classroom 3-2",
"a_sch_2_shop01" => "Blueberry Academy: School Store",
"a_sch_2_cafe01" => "Blueberry Academy: Cafeteria",
"a_sch_2_dorm01" => "Blueberry Academy: Dorm Room (Trainer)",
"a_sch_2_dorm02" => "Blueberry Academy: Dorm Room (Carmine)",
// Blueberry Academy: Terrarium
"a_su0201" => "Savanna Biome",
"a_su0202" => "Coastal Biome",
"a_su0203" => "Canyon Biome",
"a_su0204" => "Polar Biome",
_ => FormattedValue.ForceReset
};
return locations.Reset
? FormattedValue.ForceReset
: $"{playStatus} in {locations}";
return locations.Reset
? FormattedValue.ForceReset
: $"Exploring {region}{union} | {locations}";
}
private static string PokemonSV_Region(string location)
{
if (location.Contains("a_su02") || location.Contains("a_sch_2")) return "Unova";
if (location.Contains("a_su01")) return "Kitakami";
return "Paldea";
}
private static string PokemonSV_AcademyName(string title)
{
// TODO: Is this even necessary?
if (
title.Contains("Scarlet")
|| title.Contains("Escarlata")
|| title.Contains("Écarlate")
|| title.Contains("Karmesin")
|| title.Contains("Scarlatto")
|| title.Contains("スカーレット")
|| title.Contains("스칼렛")
|| title.Contains("朱")
) { return "Naranja"; }
return "Uva";
}
private static FormattedValue SuperSmashBrosUltimate_Mode(SparseMultiValue values)
@@ -641,5 +1069,7 @@ namespace Ryujinx.Ava.Systems.PlayReport
_ => FormattedValue.ForceReset
};
}
}

View File

@@ -14,7 +14,7 @@ namespace Ryujinx.Ava.Systems.PlayReport
private static readonly Lazy<Analyzer> _analyzerLazy = new(() => new Analyzer()
.AddSpec(
"01007ef00011e000",
"01007ef00011e000", // Breath of the Wild
spec => spec
.WithDescription("based on being in Master Mode.")
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
@@ -22,48 +22,74 @@ namespace Ryujinx.Ava.Systems.PlayReport
.AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
)
.AddSpec(
"0100f2c0115b6000",
"0100f2c0115b6000", // Tears of the Kingdom
spec => spec
.WithDescription("based on where you are in Hyrule (Depths, Surface, Sky).")
.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"01002da013484000",
"01002da013484000", // Skyward Sword
spec => spec
.WithDescription("based on how many Rupees you have.")
.AddValueFormatter("rupees", SkywardSwordHD_Rupees))
.AddSpec(
"0100000000010000",
"01008cf01baac000", // Echoes of Wisdom
spec => spec
.WithDescription("based on if you're playing with Assist Mode.")
.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
.WithDescription("based on where you've warped.")
.AddValueFormatter("dest_index", EchoesOfWisdom_Warp)
)
.AddSpec(
"010049900f546000", // Super Mario 3D All Stars
spec => spec
.WithDescription("based on what album and track you're listening to.")
.AddMultiValueFormatter(["app_id","song_id"], SuperMario3DAllStars_MainMenu)
)
.AddSpec(
"010075000ecbe000",
["010049900f546001", "010049900f546002", "010049900F546003"], // Super Mario 3D All Stars
spec => spec
.WithDescription("based on if you're playing with Assist Mode.")
.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
.WithDescription("based on which game you've selected to play in the collection.")
.AddValueFormatter("program_id", SuperMario3DAllStars)
)
.AddSpec(
"010028600ebda000",
"0100000000010000", // Super Mario Odyssey
spec => spec
.WithDescription("based on what kingdom you're in.")
.AddValueFormatter("stage_name", SuperMarioOdyssey)
)
.AddSpec(
"010028600ebda000", // Super Mario 3D World + Bowser's Fury
spec => spec
.WithDescription("based on being in either Super Mario 3D World or Bowser's Fury.")
.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec(
["010049900f546000", "010049900f546001", "010049900f546002", "010049900F546003"],
spec => spec
.WithDescription("based on which game you've selected to play in the collection.")
.AddValueFormatter("program_id", SuperMario3DAllStars)
)
.AddSpec(
"010015100b514000", // Super Mario Bros. Wonder
spec => spec
.WithDescription("based on what world and course you're in.")
.AddValueFormatter("stage_info", SuperMarioWonder)
)
.AddSpec( // Global & China IDs
["0100152000022000", "010075100e8ec000"],
["0100152000022000", "010075100e8ec000"], // Mario Kart 8 Deluxe
spec => spec
.WithDescription(
"based on what modes you're selecting in the menu & whether or not you're in a race.")
.AddValueFormatter("To", MarioKart8Deluxe_Mode)
)
.AddSpec(
["0100a3d008c5c000", "01008f6008c5e000"],
["0100a3d008c5c000", "01008f6008c5e000"], // Pokemon Scarlet/Violet
spec => spec
.WithDescription("based on if you're playing alone or in a group and what area of Paldea you're exploring.")
.AddMultiValueFormatter(["team_circle", "area_no"], PokemonSV)
)
.AddSpec(
"01006a800016e000",
"01006a800016e000", // Super Smash Bros. Ultimate
spec => spec
.WithDescription("based on what mode you're playing, who won, and what characters were present.")
.AddSparseMultiValueFormatter(
@@ -83,8 +109,10 @@ namespace Ryujinx.Ava.Systems.PlayReport
)
.AddSpec(
[
"0100c9a00ece6000", "01008d300c50c000", "0100d870045b6000",
"010012f017576000", "0100c62011050000", "0100b3c014bda000"
"0100B4E00444C000", "0100d870045b6000", "01008d300c50c000", "0100c62011050000", "010012f017576000",
/*Famicom*/ /*NES*/ /*SNES*/ /*GBC*/ /*GBA*/
"0100b3c014bda000", "0100c9a00ece6000", "0100e0601c632000", "0100bfc01d976000"
/*SEGA Genesis*/ /*N64*/ /*N64 MATURE*/ /*Virtual Boy*/
],
spec => spec
.WithDescription(

View File

@@ -8,6 +8,12 @@ namespace Ryujinx.Ava.UI.Helpers
internal partial class Win32NativeInterop
{
internal const int GWLP_WNDPROC = -4;
internal const int GWL_STYLE = -16;
internal const int GWL_EXSTYLE = -20;
internal const uint WS_OVERLAPPEDWINDOW = 0x00CF0000;
internal const uint WS_POPUP = 0x80000000;
internal const uint WS_VISIBLE = 0x10000000;
[Flags]
public enum ClassStyles : uint
@@ -107,9 +113,29 @@ namespace Ryujinx.Ava.UI.Helpers
nint hInstance,
nint lpParam);
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetWindowLongPtrW")]
public static partial nint GetWindowLongPtrW(nint hWnd, int nIndex);
[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 SetWindowPos(
nint hWnd,
nint hWndInsertAfter,
int x,
int y,
int cx,
int cy,
uint uFlags);
internal const uint SWP_NOZORDER = 0x0004;
internal const uint SWP_NOACTIVATE = 0x0010;
internal const uint SWP_FRAMECHANGED = 0x0020;
internal const uint SWP_NOMOVE = 0x0002;
internal const uint SWP_NOSIZE = 0x0001;
[LibraryImport("user32.dll", SetLastError = true)]
public static partial ushort GetAsyncKeyState(int nVirtKey);

View File

@@ -574,7 +574,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (Devices.Any(controller => controller.Name == name))
{
controllerNumber++;
name = GetGamepadName(gamepad, controllerNumber);
name = GetUniqueGamepadName(gamepad, ref controllerNumber);
}
return name;

View File

@@ -5,6 +5,7 @@ using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using System.Runtime.Versioning;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
@@ -2014,7 +2015,7 @@ namespace Ryujinx.Ava.UI.ViewModels
LastFullscreenToggle = Environment.TickCount64;
if (WindowState is not WindowState.Normal)
if (WindowState is WindowState.FullScreen)
{
WindowState = WindowState.Normal;
Window.TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowOldUI;
@@ -2023,21 +2024,74 @@ namespace Ryujinx.Ava.UI.ViewModels
{
ShowMenuAndStatusBar = true;
}
if (OperatingSystem.IsWindows())
{
RestoreWindowFromFullscreen();
}
}
else
{
WindowState = WindowState.FullScreen;
Window.TitleBar.ExtendsContentIntoTitleBar = true;
if (IsGameRunning)
{
ShowMenuAndStatusBar = false;
}
if (OperatingSystem.IsWindows())
{
MakeWindowFullscreen();
}
else
{
WindowState = WindowState.FullScreen;
}
}
IsFullScreen = WindowState is WindowState.FullScreen;
}
private nint _savedWindowStyle;
[SupportedOSPlatform("windows")]
private void MakeWindowFullscreen()
{
nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero;
if (hwnd == nint.Zero) return;
// Save current style and placement
_savedWindowStyle = Win32NativeInterop.GetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE);
// Remove window chrome: WS_OVERLAPPEDWINDOW -> WS_POPUP | WS_VISIBLE
Win32NativeInterop.SetWindowLongPtrW(hwnd, Win32NativeInterop.GWL_STYLE,
unchecked((nint)(Win32NativeInterop.WS_POPUP | Win32NativeInterop.WS_VISIBLE)));
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,
Win32NativeInterop.SWP_NOZORDER | Win32NativeInterop.SWP_NOACTIVATE | Win32NativeInterop.SWP_FRAMECHANGED);
WindowState = WindowState.FullScreen;
}
[SupportedOSPlatform("windows")]
private void RestoreWindowFromFullscreen()
{
nint hwnd = Window.TryGetPlatformHandle()?.Handle ?? nint.Zero;
if (hwnd == nint.Zero) return;
// 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);
}
public static void SaveConfig()
{
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);

View File

@@ -273,6 +273,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableTrace { get; set; }
public bool EnableGuest { get; set; }
public bool EnableFsAccessLog { get; set; }
public bool EnableNetLog { get; set; }
public bool EnableAvaloniaLog { get; set; }
public bool EnableDebug { get; set; }
public bool IsOpenAlEnabled { get; set; }
@@ -725,6 +726,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableGuest = config.Logger.EnableGuest;
EnableDebug = config.Logger.EnableDebug;
EnableFsAccessLog = config.Logger.EnableFsAccessLog;
EnableNetLog = config.Logger.EnableNetLog;
EnableAvaloniaLog = config.Logger.EnableAvaloniaLog;
FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode;
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
@@ -848,6 +850,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Logger.EnableGuest.Value = EnableGuest;
config.Logger.EnableDebug.Value = EnableDebug;
config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog;
config.Logger.EnableNetLog.Value = EnableNetLog;
config.Logger.EnableAvaloniaLog.Value = EnableAvaloniaLog;
config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode;
config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel;

View File

@@ -70,6 +70,10 @@
ToolTip.Tip="{ext:Locale FileAccessLogTooltip}">
<TextBlock Text="{ext:Locale SettingsTabLoggingEnableFsAccessLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableNetLog}"
ToolTip.Tip="{ext:Locale NetLogTooltip}">
<TextBlock Text="{ext:Locale SettingsTabLoggingEnableNetLogs}" />
</CheckBox>
<CheckBox IsChecked="{Binding EnableDebug}"
ToolTip.Tip="{ext:Locale DebugLogTooltip}">
<TextBlock Text="{ext:Locale SettingsTabLoggingEnableDebugLogs}" />