Compare commits

..

41 Commits

Author SHA1 Message Date
GreemDev
b7dd718d6f use lambda-based config modifier insstead of manually setting a bool to true 2025-12-22 00:13:53 -06:00
GreemDev
6ee7957574 use a helper to get key path instead of checking mode & userpath existing every time 2025-12-19 23:15:23 -06:00
GreemDev
bf62531802 unused property 2025-12-19 23:15:23 -06:00
GreemDev
17be50ea80 Update title with page 2025-12-19 23:15:23 -06:00
GreemDev
ec50a1ec3e rename markup imported xml namespace to ext to match the rest of the codebase 2025-12-19 23:15:23 -06:00
GreemDev
5a20047e5e forgot to string.Format gamedir page desc 2025-12-19 23:15:23 -06:00
GreemDev
f9fed4cf4d make page desc smaller 2025-12-19 23:15:23 -06:00
GreemDev
2970dcd3c7 Localize all (I think...) previously hardcoded english strings in the setup wizard 2025-12-19 23:15:23 -06:00
GreemDev
4be6cb2fa1 oops 2025-12-19 23:15:23 -06:00
GreemDev
c90d2af9cd game dir setup
known bugs are a missing prod.keys popup after setup (how), as well as the dialog for autoload kinda cluttering up the screen after you hit next on the game dir page
2025-12-19 23:15:23 -06:00
GreemDev
13ff9cb162 Separate firmware avatar loading from the selector view model
This is going to be used for the setup wizard (probably)
2025-12-19 23:15:23 -06:00
GreemDev
b35ba58831 rewrite EmbeddedAvaloniaResources 2025-12-19 23:15:23 -06:00
GreemDev
e12a77d4a3 add a setup finished screen
added the ability to hide the help button (basically just for the finish screen, because it has a bigger discord button in the same place)
holding shift while opening the setup wizard now opens it in passive mode, aka it will install only what you need. this is mostly for testing and likely will be nuked before this code as a whole is made part of the official emulator, but it might not
2025-12-19 23:15:23 -06:00
GreemDev
804a4e0bcb reduce logo crunching 2025-12-19 23:15:23 -06:00
GreemDev
94870eafaa further simplify pagebuilding by embedding the desired title locale key in the context base type 2025-12-19 23:15:23 -06:00
GreemDev
7e6cc31866 cleanup usings 2025-12-19 23:15:23 -06:00
GreemDev
3b25c43abf reorganize RyujinxSetupWizard
additionally, the CreateHelpContent method is now no longer locked to returning an Avalonia `Control`.
the WithHelpContent method also has logic to handle it returning a string directly, and will wrap it in a textblock with h1 style and size 20 font. otherwise it's up to the ContentPresenter for the help content to choose how to display it if it's none of the above mentioned types.
2025-12-19 23:15:23 -06:00
GreemDev
1804dd031b oops
i left in my very professional logger debugging
2025-12-19 23:15:23 -06:00
GreemDev
211498e060 Overhaul setup wizard help pages
the context can now override a virtual method named `CreateHelpContent` which the setup wizard system will automatically try to use when you use the generic overload taking a generic context type. If the return is null, it skips setting entirely (the default impl is null)

additionally made the discord join link a button with code copied from the about window, and made it centered at the bottom.
2025-12-19 23:15:23 -06:00
GreemDev
4bdee89288 small cleanup 2025-12-19 23:15:23 -06:00
GreemDev
d8a6364cca rename NotificationHelper to RyujinxNotificationManager,
rename instance method names.
Additionally clarified what the math is in the notification manager margin parameter.
2025-12-19 23:15:23 -06:00
GreemDev
2f794794c6 use the margin to force it to show bottom center
(boy i sure do hope this doesnt have any adverse effects on anything but my specific resolution & scaling configuration!)
2025-12-19 23:15:23 -06:00
GreemDev
1d6c2426df OOPS broke the setup wizard :3 2025-12-19 23:15:23 -06:00
GreemDev
6cd03f15fa cleanup 2025-12-19 23:15:23 -06:00
GreemDev
3fe7600382 add "overwrite mode" for the setup wizard, basically this just ignores the precondition of having whatever the page configures before showing it.
i.e. if you had keys installed, previously it'd skip right to firmware.

additionally added more customization to the now instance-based NotificationHelper
2025-12-19 23:15:23 -06:00
GreemDev
dc2aa837b3 Setup Wizard restructuring
- Remove polymorphic base, this only existed because TKMM has a desktop/switch setup prodecure difference and has 2 implementations of the setup wizard. We only need one.
- Remove Systems/UI file split, they're all in Ryujinx.Ava.UI now
- made NotificationHelper instance-based to allow you to encapsulate notifications to a window that magically disappear when the window is closed, instead of switching to showing on the main window.
2025-12-19 23:15:22 -06:00
GreemDev
133ac41425 Bake setup step logic into the view models themselves instead of being in the setup wizard implementation
renamed view models to contexts (like TKMM), however the contexts here are actually of a unique base type; containing aforementioned setup step logic. if the return value is of an error state result, it will prompt a retry of the page.
2025-12-19 23:15:22 -06:00
GreemDev
fd2ecee479 fix "could not find part of path" error when installing firmware 2025-12-19 23:15:22 -06:00
GreemDev
8f529d17a8 combine SetupWizardPage and the builder type since the builder mutated an instance of the built type anyways 2025-12-19 23:15:22 -06:00
GreemDev
884d0f526c treat configuration load fail as first start (so you're prompted to set the game/autoload dirs, when that step is implemented) 2025-12-19 23:15:22 -06:00
GreemDev
c5b325bde2 add a setup wizard opener in the help dropdown in the menu bar, that also respects CanShowSetupWizard 2025-12-19 23:15:22 -06:00
GreemDev
8ab851ead8 move more of the setup wizard logic into the setup wizard itself instead of having some critical logic in a random lambda in MainWindow.axaml.cs 2025-12-19 23:15:22 -06:00
GreemDev
5a060cf451 fixup namespaces (again) 2025-12-19 23:15:22 -06:00
GreemDev
9b0fa3bf6d content & viewmodel object creation helper with out param, touch up firmware install step 2025-12-19 23:15:22 -06:00
GreemDev
325e13a490 fix: require valid key installations before moving onto firmware setup step 2025-12-19 23:15:22 -06:00
GreemDev
e202cccc6e firmware stage 2025-12-19 23:15:22 -06:00
GreemDev
e0ed8f56ea cleanup 2025-12-19 23:15:22 -06:00
GreemDev
46b2fb92d7 more namespace fixes 2025-12-19 23:15:22 -06:00
GreemDev
8563e7d4dc use a custom key install function with notifications instead of the normal one with dialogs 2025-12-19 23:15:22 -06:00
GreemDev
ee10cbf735 cleanup 2025-12-19 23:15:22 -06:00
GreemDev
b033adbde7 Initial work on a setup wizard
Setup wizard abstraction & architecture from TKMM
2025-12-19 23:15:22 -06:00
100 changed files with 3001 additions and 2253 deletions

View File

@@ -63,6 +63,7 @@ jobs:
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
echo "commit_message=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
shell: bash
- name: Configure for release
@@ -89,7 +90,7 @@ jobs:
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -103,7 +104,7 @@ jobs:
tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
shell: bash
- name: Build AppImage (Linux)
@@ -140,7 +141,7 @@ jobs:
mv Ryujinx.AppImage ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
shell: bash
macos_release:
@@ -201,7 +202,7 @@ jobs:
- name: Publish macOS Ryujinx
run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -p publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
create_gitlab_release:
name: Create GitLab Release
@@ -228,11 +229,12 @@ jobs:
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
echo "commit_message=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
shell: bash
- name: Create tag
run: |
gli create-tag -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Canary-${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }}
gli create-tag -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Canary-${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }} -c "${{ steps.version_info.outputs.commit_message }}"
- name: Create release
run: |

View File

@@ -86,7 +86,7 @@ jobs:
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -100,7 +100,7 @@ jobs:
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -139,7 +139,7 @@ jobs:
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
popd
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -p release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
shell: bash
macos_release:
@@ -203,7 +203,7 @@ jobs:
run: |
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -p publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
create_gitlab_release:
name: Create GitLab Release

View File

@@ -44,7 +44,7 @@
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.126" />
<PackageVersion Include="Ryujinx.UpdateClient" Version="1.0.44" />
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
<PackageVersion Include="Gommon" Version="2.8.0.1" />
<PackageVersion Include="Gommon" Version="2.8.0.2" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.11.1" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />
@@ -59,4 +59,4 @@
<PackageVersion Include="System.Management" Version="9.0.2" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
</ItemGroup>
</Project>
</Project>

View File

@@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.GAL", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.OpenGL", "src\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj", "{9558FB96-075D-4219-8FFF-401979DC0B69}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.RenderDoc", "src\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj", "{D58FA894-27D5-4EAA-9042-AD422AD82931}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Texture", "src\Ryujinx.Graphics.Texture\Ryujinx.Graphics.Texture.csproj", "{E1B1AD28-289D-47B7-A106-326972240207}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Shader", "src\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj", "{03B955CD-AD84-4B93-AAA7-BF17923BBAA5}"
@@ -557,18 +555,6 @@ Global
{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x64.Build.0 = Release|Any CPU
{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.ActiveCfg = Release|Any CPU
{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.Build.0 = Release|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.ActiveCfg = Debug|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.Build.0 = Debug|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.ActiveCfg = Debug|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.Build.0 = Debug|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.Build.0 = Release|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.ActiveCfg = Release|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -1,24 +0,0 @@
{
"Languages": {
"ar_SA": "اَلْعَرَبِيَّةُ",
"de_DE": "Deutsch",
"el_GR": "Ελληνικά",
"en_US": "English (US)",
"es_ES": "Español (ES)",
"fr_FR": "Français (FR)",
"he_IL": "עִברִית",
"it_IT": "Italiano",
"ja_JP": "日本語",
"ko_KR": "한국어",
"no_NO": "Norsk",
"pl_PL": "Polski",
"pt_BR": "Português (BR)",
"ru_RU": "Русский",
"sv_SE": "Svenska",
"th_TH": "ภาษาไทย",
"tr_TR": "Türkçe",
"uk_UA": "Українська",
"zh_CN": "简体中文",
"zh_TW": "繁體中文 (台灣)"
}
}

View File

@@ -1,60 +0,0 @@
# Ryubing Locales
Ryubing Locales uses a custom format, which uses a file for defining the supported languages and a folder of json files for the locales themselves.
Each json file holds the locales for a specific part of the emulator, e.g. the Setup Wizard locales are in `SetupWizard.json`, and each locale entry in the file includes all the supported languages in the same place.
## Languages
in the `/assets/` folder you will find the `Languages.json` file, which defines all the languages supported by the emulator.
The file includes a table of the langauge codes and their langauge names.
#Example of the format for Languages.json
{
"Languages": {
"ar_SA": "اَلْعَرَبِيَّةُ",
"en_US": "English (US)",
...
"zh_TW": "繁體中文 (台灣)"
}
}
## Locales
in the `/assets/Locales/` folder you will find the json files, which define all the locales supported by the emulator.
Each json file holds locales for a specific part of the emulator in a large array of locale objects.
Each locale is made up an ID used for lookup and a list of the languages and their matching translations.
Any empty string or null value will automatically use the English translation instead in the emulator.
### Format
When adding a new locale, you just need to add the ID and the en_US language translation, then the validation system will add default values for the rest of languages automatically, when rebuilding the project.
If you want to signal that a translation is supposed to match the English translation, you just have to replace the empty string with `null`.
When you want to check what translations are missing for a language just search for `"<lang_code>": ""`, e.g: `"en_US": ""` (but with any other language, as English will never be missing translations).
### Legacy file (Root.json)
Currently all older locales are stored in `Root.json`, but they are slowly being moved into newer, more descriptive json files, to make the locale system more accessible.
Do **not** add new locales to `Root.json`.
If no json file exists for the specific part of the emulator you're working on, you should instead add a new json file for that part.
#Example of the format for Root.json
{
"Locales": [
{
"ID": "MenuBarActionsOpenMiiEditor",
"Translations": {
"ar_SA": "",
"en_US": "Mii Editor",
...
"zh_TW": "Mii 編輯器"
}
},
{
"ID": "KeyNumber9",
"Translations": {
"ar_SA": "٩",
"en_US": "9",
...
"zh_TW": null
}
}
]
}

View File

@@ -1,104 +0,0 @@
{
"Locales": [
{
"ID": "MenuBarActions_StartCapture",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Start RenderDoc Frame Capture",
"es_ES": "",
"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": "MenuBarActions_EndCapture",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "End RenderDoc Frame Capture",
"es_ES": "",
"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": "MenuBarActions_DiscardCapture",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Discard RenderDoc Frame Capture",
"es_ES": "",
"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": "MenuBarActions_DiscardCapture_ToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.",
"es_ES": "",
"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": ""
}
}
]
}

View File

@@ -1,5 +1,72 @@
{
"Info": {
"Format1": "The Locale file uses a custom Unified format.",
"Format2": "The file starts with a list of all the supported languages.",
"Format3": "Each locale is made up an ID used for lookup and a list",
"Format4": "of the languages and their matching translations.",
"Format5": "When adding a new locale you just need to add the ID and",
"Format6": "the en_US language translation, then the validation system",
"Format7": "will add the rest of the languages automatically on rebuild.",
"Format8": "By default the languages will be added with an empty string.",
"Format9": "Any empty string or null value will automatically match the",
"Format10": "English translation.",
"Format11": "If you want to signal that a translation is supposed to",
"Format12": "match the English translation, you just have to replace the",
"Format13": "empty string with null.",
"Format14": "Translators who want to check what translations are missing",
"Format15": "for their language just need to search for:",
"Format16": "{'lang_code': ''} with double quotes instead of single",
"Format17": "e.g: {'en_US': ''} (but with any other language as English",
"Format18": "will never be missing translations)."
},
"Languages": [
"ar_SA",
"de_DE",
"el_GR",
"en_US",
"es_ES",
"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"
],
"Locales": [
{
"ID": "Language",
"Translations": {
"ar_SA": "اَلْعَرَبِيَّةُ",
"de_DE": "Deutsch",
"el_GR": "Ελληνικά",
"en_US": "English (US)",
"es_ES": "Español (ES)",
"fr_FR": "Français (FR)",
"he_IL": "עִברִית",
"it_IT": "Italiano",
"ja_JP": "日本語",
"ko_KR": "한국어",
"no_NO": "Norsk",
"pl_PL": "Polski",
"pt_BR": "Português (BR)",
"ru_RU": "Русский",
"sv_SE": "Svenska",
"th_TH": "ภาษาไทย",
"tr_TR": "Türkçe",
"uk_UA": "Українська",
"zh_CN": "简体中文",
"zh_TW": "繁體中文 (台灣)"
}
},
{
"ID": "MenuBarActionsOpenMiiEditor",
"Translations": {
@@ -24774,6 +24841,781 @@
"zh_CN": "如果您在设置中有某些 LDN 口令则可加入此游戏。",
"zh_TW": "你只能加入與 LDN 網路密碼片語 (passphrase) 設定相同的遊戲。"
}
},
{
"ID": "SetupWizardOpen",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Setup Wizard",
"es_ES": "",
"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": "SetupWizardActionBack",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Back",
"es_ES": "",
"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": "SetupWizardActionNext",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Next",
"es_ES": "",
"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": "SetupWizardFirstPageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Welcome to Ryubing!",
"es_ES": "",
"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": "SetupWizardFirstPageContent",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Ryubing is a fork of the discontinued Nintendo Switch emulator, Ryujinx.\n\nThis setup wizard will guide you through the necessary steps needed for Ryubing to play your Switch games on PC.",
"es_ES": "",
"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": "SetupWizardFirstPageAction",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Start Setup",
"es_ES": "",
"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": "SetupWizardKeysPageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Key Files",
"es_ES": "",
"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": "SetupWizardKeysPageDescription",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder containing your prod/title .keys files:",
"es_ES": "",
"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": "SetupWizardKeysPageFolderPopupTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder containing your prod/title .keys files",
"es_ES": "",
"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": "SetupWizardKeysPageHelpText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Not sure how to get your keys?",
"es_ES": "",
"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": "SetupWizardKeysPageSkipText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Skipped setting up keys as you already have a valid key installation and did not choose a folder to install from.\nClick '{0}' if you wish to reinstall your keys.",
"es_ES": "",
"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": "SetupWizardFirmwarePageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Switch Firmware",
"es_ES": "",
"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": "SetupWizardFirmwarePageDescription",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder or .zip/.xci containing your dumped Nintendo Switch firmware:",
"es_ES": "",
"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": "SetupWizardFirmwarePageFolderPopupTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder containing your dumped & extracted Switch firmware.",
"es_ES": "",
"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": "SetupWizardFirmwarePageFilePopupTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the file containing your dumped Switch firmware.",
"es_ES": "",
"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": "SetupWizardFirmwarePageFileBrowse",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Select .zip or .xci",
"es_ES": "",
"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": "SetupWizardFirmwarePageFolderBrowse",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Select Extracted Folder",
"es_ES": "",
"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": "SetupWizardFirmwarePageInstallSuccessNotificationTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Firmware installed",
"es_ES": "",
"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": "SetupWizardFirmwarePageInstallSuccessNotificationText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Installed firmware version {0}.",
"es_ES": "",
"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": "SetupWizardFirmwarePageInstallFailNotificationTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Firmware not installed",
"es_ES": "",
"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": "SetupWizardFirmwarePageInstallFailNotificationText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "It seems some error occurred when trying to install the firmware at path '{0}'.\nDid that folder contain a firmware dump?",
"es_ES": "",
"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": "SetupWizardFirmwarePageHelpText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Not sure how to get your firmware off of your Switch?",
"es_ES": "",
"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": "SetupWizardFirmwarePageSkipText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Skipped setting up firmware as you already have a valid firmware installation and did not choose a folder or file to install from.\nClick '{0}' if you wish to overwrite your firmware.",
"es_ES": "",
"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": "SetupWizardGameDirsPageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Game, Update, and DLC Paths",
"es_ES": "",
"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": "SetupWizardGameDirsPageDescription",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "{0} can be pointed at any number of folders to look for your games, updates, and DLC content.\nAt least one folder must be specified in game directories before continuing.",
"es_ES": "",
"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": "SetupWizardGameDirsPageNoFoldersSelectedError",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "At least one folder for games must be selected; otherwise the UI will be empty.",
"es_ES": "",
"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": "SetupWizardGameDirsPageHelpText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Not sure how to get your games, updates, and/or DLC onto your PC?",
"es_ES": "",
"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": "SetupWizardFinalPageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Setup Complete",
"es_ES": "",
"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": "SetupWizardFinalPageDescription",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Your installation of Ryubing (aka Ryujinx) has been completed.\n\nIf you require assistance, feel free to join our Discord server and ask for help,\nafter verifying your possession of a modded Nintendo Switch.",
"es_ES": "",
"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": "SetupWizardFinalPageAction",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Finish",
"es_ES": "",
"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": "SetupWizardHelpLinkButton",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Click here to view a guide.",
"es_ES": "",
"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": ""
}
}
]
}
}

View File

@@ -11,7 +11,9 @@ namespace Ryujinx.BuildValidationTasks
{
static readonly JsonSerializerOptions _jsonOptions = new()
{
WriteIndented = true, NewLine = "\n", Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
WriteIndented = true,
NewLine = "\n",
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
public LocalesValidationTask() { }
@@ -20,116 +22,77 @@ namespace Ryujinx.BuildValidationTasks
{
Console.WriteLine("Running Locale Validation Task...");
bool encounteredIssue = false;
string langPath = projectPath + "assets/Languages.json";
string path = projectPath + "assets/locales.json";
string data;
using (StreamReader sr = new(langPath))
using (StreamReader sr = new(path))
{
data = sr.ReadToEnd();
}
if (isGitRunner && data.Contains("\r\n"))
throw new FormatException("Languages.json is using CRLF line endings! It should be using LF line endings, rebuild locally to fix...");
LocalesJson json;
LanguagesJson langJson;
if (isGitRunner && data.Contains("\r\n"))
throw new FormatException("locales.json is using CRLF line endings! It should be using LF line endings, rebuild locally to fix...");
try
{
langJson = JsonSerializer.Deserialize<LanguagesJson>(data);
json = JsonSerializer.Deserialize<LocalesJson>(data);
}
catch (JsonException e)
{
throw new JsonException(e.Message); //shorter and easier stacktrace
}
foreach ((string code, string lang) in langJson.Languages)
bool encounteredIssue = false;
for (int i = 0; i < json.Locales.Count; i++)
{
if (string.IsNullOrEmpty(lang))
LocalesEntry locale = json.Locales[i];
foreach (string langCode in json.Languages.Where(lang => !locale.Translations.ContainsKey(lang)))
{
throw new JsonException($"{code} language name missing!");
encounteredIssue = true;
if (!isGitRunner)
{
locale.Translations.Add(langCode, string.Empty);
Console.WriteLine($"Added '{langCode}' to Locale '{locale.ID}'");
}
else
{
Console.WriteLine($"Missing '{langCode}' in Locale '{locale.ID}'!");
}
}
foreach (string langCode in json.Languages.Where(lang => locale.Translations.ContainsKey(lang) && lang != "en_US" && locale.Translations[lang] == locale.Translations["en_US"]))
{
encounteredIssue = true;
if (!isGitRunner)
{
locale.Translations[langCode] = string.Empty;
Console.WriteLine($"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
}
else
{
Console.WriteLine($"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
}
}
locale.Translations = locale.Translations.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value);
json.Locales[i] = locale;
}
string folderPath = projectPath + "assets/Locales/";
if (isGitRunner && encounteredIssue)
throw new JsonException("1 or more locales are invalid! Rebuild locally to fix...");
string[] paths = Directory.GetFiles(folderPath, "*.json", SearchOption.AllDirectories);
string jsonString = JsonSerializer.Serialize(json, _jsonOptions);
foreach (string path in paths)
using (StreamWriter sw = new(path))
{
using (StreamReader sr = new(path))
{
data = sr.ReadToEnd();
}
if (isGitRunner && data.Contains("\r\n"))
throw new FormatException($"{Path.GetFileName(path)} is using CRLF line endings! It should be using LF line endings, rebuild locally to fix...");
LocalesJson json;
try
{
json = JsonSerializer.Deserialize<LocalesJson>(data);
}
catch (JsonException e)
{
throw new JsonException(e.Message); //shorter and easier stacktrace
}
for (int i = 0; i < json.Locales.Count; i++)
{
LocalesEntry locale = json.Locales[i];
foreach (string langCode in
langJson.Languages.Keys.Where(lang => !locale.Translations.ContainsKey(lang)))
{
encounteredIssue = true;
if (!isGitRunner)
{
locale.Translations.Add(langCode, string.Empty);
Console.WriteLine($"Added '{langCode}' to Locale '{locale.ID}'");
}
else
{
Console.WriteLine($"Missing '{langCode}' in Locale '{locale.ID}'!");
}
}
foreach (string langCode in langJson.Languages.Keys.Where(lang =>
locale.Translations.ContainsKey(lang) && lang != "en_US" &&
locale.Translations[lang] == locale.Translations["en_US"]))
{
encounteredIssue = true;
if (!isGitRunner)
{
locale.Translations[langCode] = string.Empty;
Console.WriteLine(
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
}
else
{
Console.WriteLine(
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
}
}
locale.Translations = locale.Translations.OrderBy(pair => pair.Key)
.ToDictionary(pair => pair.Key, pair => pair.Value);
json.Locales[i] = locale;
}
if (isGitRunner && encounteredIssue)
throw new JsonException("1 or more locales are invalid! Rebuild locally to fix...");
string jsonString = JsonSerializer.Serialize(json, _jsonOptions);
using (StreamWriter sw = new(path))
{
sw.Write(jsonString);
}
sw.Write(jsonString);
}
Console.WriteLine("Finished Locale Validation Task!");
@@ -137,13 +100,10 @@ namespace Ryujinx.BuildValidationTasks
return true;
}
struct LanguagesJson
{
public Dictionary<string, string> Languages { get; set; }
}
struct LocalesJson
{
public Dictionary<string, string> Info { get; set; }
public List<string> Languages { get; set; }
public List<LocalesEntry> Locales { get; set; }
}

View File

@@ -91,7 +91,11 @@ namespace Ryujinx.Common
public void Dispose()
{
_queue.CompleteAdding();
try
{
_queue.CompleteAdding();
} catch (ObjectDisposedException) {}
_cts.Cancel();
_workerThread.Join();

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonConverter(typeof(JsonStringEnumConverter<AntiAliasing>))]
[JsonConverter(typeof(TypedStringEnumConverter<AntiAliasing>))]
public enum AntiAliasing
{
None,

View File

@@ -31,6 +31,11 @@ namespace Ryujinx.Common.Configuration
public static string KeysDirPath { get; private set; }
public static string KeysDirPathUser { get; }
public static string GetKeysDir() =>
Mode is LaunchMode.UserProfile && Directory.Exists(KeysDirPathUser)
? KeysDirPathUser
: KeysDirPath;
public static string LogsDirPath { get; private set; }
public const string DefaultNandDir = "bis";

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonConverter(typeof(JsonStringEnumConverter<AspectRatio>))]
[JsonConverter(typeof(TypedStringEnumConverter<AspectRatio>))]
public enum AspectRatio
{
Fixed4x3,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonConverter(typeof(JsonStringEnumConverter<BackendThreading>))]
[JsonConverter(typeof(TypedStringEnumConverter<BackendThreading>))]
public enum BackendThreading
{
Auto,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonConverter(typeof(JsonStringEnumConverter<GraphicsBackend>))]
[JsonConverter(typeof(TypedStringEnumConverter<GraphicsBackend>))]
public enum GraphicsBackend
{
Vulkan,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonConverter(typeof(JsonStringEnumConverter<GraphicsDebugLevel>))]
[JsonConverter(typeof(TypedStringEnumConverter<GraphicsDebugLevel>))]
public enum GraphicsDebugLevel
{
None,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid.Controller
{
[JsonConverter(typeof(JsonStringEnumConverter<GamepadInputId>))]
[JsonConverter(typeof(TypedStringEnumConverter<GamepadInputId>))]
public enum GamepadInputId : byte
{
Unbound,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
{
[JsonConverter(typeof(JsonStringEnumConverter<MotionInputBackendType>))]
[JsonConverter(typeof(TypedStringEnumConverter<MotionInputBackendType>))]
public enum MotionInputBackendType : byte
{
Invalid,

View File

@@ -1,14 +1,15 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid.Controller
{
[JsonConverter(typeof(JsonStringEnumConverter<StickInputId>))]
[JsonConverter(typeof(TypedStringEnumConverter<StickInputId>))]
public enum StickInputId : byte
{
Unbound,
Left,
Right,
Count,
}
}

View File

@@ -1,3 +1,4 @@
using Ryujinx.Common.Utilities;
using System;
using System.Text.Json.Serialization;
@@ -5,7 +6,7 @@ namespace Ryujinx.Common.Configuration.Hid
{
// This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
[Flags]
[JsonConverter(typeof(JsonStringEnumConverter<ControllerType>))]
[JsonConverter(typeof(TypedStringEnumConverter<ControllerType>))]
public enum ControllerType
{
None,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid
{
[JsonConverter(typeof(JsonStringEnumConverter<InputBackendType>))]
[JsonConverter(typeof(TypedStringEnumConverter<InputBackendType>))]
public enum InputBackendType
{
Invalid,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid
{
[JsonConverter(typeof(JsonStringEnumConverter<Key>))]
[JsonConverter(typeof(TypedStringEnumConverter<Key>))]
public enum Key
{
Unknown,

View File

@@ -1,9 +1,10 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid
{
// This enum was duplicated from Ryujinx.HLE.HOS.Services.Hid.PlayerIndex and should be kept identical
[JsonConverter(typeof(JsonStringEnumConverter<PlayerIndex>))]
[JsonConverter(typeof(TypedStringEnumConverter<PlayerIndex>))]
public enum PlayerIndex
{
Player1 = 0,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonConverter(typeof(JsonStringEnumConverter<MemoryManagerMode>))]
[JsonConverter(typeof(TypedStringEnumConverter<MemoryManagerMode>))]
public enum MemoryManagerMode : byte
{
SoftwarePageTable,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration
{
[JsonConverter(typeof(JsonStringEnumConverter<ScalingFilter>))]
[JsonConverter(typeof(TypedStringEnumConverter<ScalingFilter>))]
public enum ScalingFilter
{
Bilinear,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Logging
{
[JsonConverter(typeof(JsonStringEnumConverter<LogClass>))]
[JsonConverter(typeof(TypedStringEnumConverter<LogClass>))]
public enum LogClass
{
Application,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Logging
{
[JsonConverter(typeof(JsonStringEnumConverter<LogLevel>))]
[JsonConverter(typeof(TypedStringEnumConverter<LogLevel>))]
public enum LogLevel
{
Debug,

View File

@@ -13,6 +13,15 @@ namespace Ryujinx.Common
public const string SetupGuideWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
public const string DumpKeysWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Keys";
public const string DumpFirmwareWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Firmware";
public const string DumpContentWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Dumping/Games,-Updates-&-DLC";
public const string MultiplayerWikiUrl =
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
}

View File

@@ -127,7 +127,7 @@ namespace Ryujinx.Common
public static string[] GetAllAvailableResources(string path, string ext = "")
{
return ResolveManifestPath(path).Item1.GetManifestResourceNames()
.Where(r => r.StartsWith(path.Replace('/', '.')) && r.EndsWith(ext))
.Where(r => r.EndsWith(ext))
.ToArray();
}

View File

@@ -0,0 +1,37 @@
#nullable enable
using Ryujinx.Common.Logging;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Utilities
{
/// <summary>
/// Specifies that value of <see cref="TEnum"/> will be serialized as string in JSONs
/// </summary>
/// <remarks>
/// Trimming friendly alternative to <see cref="JsonStringEnumConverter"/>.
/// Get rid of this converter if dotnet supports similar functionality out of the box.
/// </remarks>
/// <typeparam name="TEnum">Type of enum to serialize</typeparam>
public sealed class TypedStringEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum : struct, Enum
{
public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? enumValue = reader.GetString();
if (Enum.TryParse(enumValue, out TEnum value))
{
return value;
}
Logger.Warning?.Print(LogClass.Configuration, $"Failed to parse enum value \"{enumValue}\" for {typeof(TEnum)}, using default \"{default(TEnum)}\"");
return default;
}
public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
}

View File

@@ -1,12 +0,0 @@
using System;
namespace Ryujinx.Graphics.RenderDocApi
{
public readonly record struct Capture(int Index, string FileName, DateTime Timestamp)
{
public void SetComments(string comments)
{
RenderDoc.SetCaptureFileComments(FileName, comments);
}
}
}

View File

@@ -1,100 +0,0 @@
// ReSharper disable UnusedMember.Global
namespace Ryujinx.Graphics.RenderDocApi
{
public enum CaptureOption
{
/// <summary>
/// specifies whether the application is allowed to enable vsync. Default is on.
/// </summary>
AllowVsync = 0,
/// <summary>
/// specifies whether the application is allowed to enter exclusive fullscreen. Default is on.
/// </summary>
AllowFullscreen = 1,
/// <summary>
/// specifies whether (where possible) API-specific debugging is enabled. Default is off.
/// </summary>
ApiValidation = 2,
/// <summary>
/// specifies whether each API call should save a callstack. Default is off.
/// </summary>
CaptureCallstacks = 3,
/// <summary>
/// specifies whether, if <see cref="CaptureCallstacks"/> is enabled, callstacks are only saved on actions. Default is off.
/// </summary>
CaptureCallstacksOnlyDraws = 4,
/// <summary>
/// specifies a delay in seconds after launching a process to pause, to allow debuggers to attach. <br/>
/// This will only apply to child processes since the delay happens at process startup. Default is 0.
/// </summary>
DelayForDebugger = 5,
/// <summary>
/// specifies whether any mapped memory updates should be bounds-checked for overruns,
/// and uninitialised buffers are initialized to <code>0xDDDDDDDD</code> to catch use of uninitialised data.
/// Only supported on D3D11 and OpenGL. Default is off.
/// </summary>
/// <remarks>
/// This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do
/// not do the same kind of interception &amp; checking, and undefined contents are really undefined.
/// </remarks>
VerifyBufferAccess = 6,
/// <summary>
/// Hooks any system API calls that create child processes, and injects
/// RenderDoc into them recursively with the same options.
/// </summary>
HookIntoChildren = 7,
/// <summary>
/// specifies whether all live resources at the time of capture should be included in the capture,
/// even if they are not referenced by the frame. Default is off.
/// </summary>
RefAllSources = 8,
/// <summary>
/// By default, RenderDoc skips saving initial states for resources where the
/// previous contents don't appear to be used, assuming that writes before
/// reads indicate previous contents aren't used.
/// </summary>
/// <remarks>
/// **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or
/// getting it will be ignored, to allow compatibility with older versions.
/// In v1.1 the option acts as if it's always enabled.
/// </remarks>
SaveAllInitials = 9,
/// <summary>
/// In APIs that allow for the recording of command lists to be replayed later,
/// RenderDoc may choose to not capture command lists before a frame capture is
/// triggered, to reduce overheads. This means any command lists recorded once
/// and replayed many times will not be available and may cause a failure to
/// capture.
/// </summary>
/// <remarks>
/// NOTE: This is only true for APIs where multithreading is difficult or
/// discouraged. Newer APIs like Vulkan and D3D12 will ignore this option
/// and always capture all command lists since the API is heavily oriented
/// around it and the overheads have been reduced by API design.
/// </remarks>
CaptureAllCmdLists = 10,
/// <summary>
/// Mute API debugging output when the <see cref="ApiValidation"/> option is enabled.
/// </summary>
DebugOutputMute = 11,
/// <summary>
/// Allow vendor extensions to be used even when they may be
/// incompatible with RenderDoc and cause corrupted replays or crashes.
/// </summary>
AllowUnsupportedVendorExtensions = 12,
/// <summary>
/// Define a soft memory limit which some APIs may aim to keep overhead under where
/// possible. Anything above this limit will where possible be saved directly to disk during
/// capture.<br/>
/// This will cause increased disk space use (which may cause a capture to fail if disk space is
/// exhausted) as well as slower capture times.
/// <br/><br/>
/// Not all memory allocations may be deferred like this so it is not a guarantee of a memory
/// limit.
/// <br/><br/>
/// Units are in MBs, suggested values would range from 200MB to 1000MB.
/// </summary>
SoftMemoryLimit = 13,
}
}

View File

@@ -1,83 +0,0 @@
// ReSharper disable UnusedMember.Global
namespace Ryujinx.Graphics.RenderDocApi
{
public enum InputButton
{
// '0' - '9' matches ASCII values
Key0 = 0x30,
Key1 = 0x31,
Key2 = 0x32,
Key3 = 0x33,
Key4 = 0x34,
Key5 = 0x35,
Key6 = 0x36,
Key7 = 0x37,
Key8 = 0x38,
Key9 = 0x39,
// 'A' - 'Z' matches ASCII values
A = 0x41,
B = 0x42,
C = 0x43,
D = 0x44,
E = 0x45,
F = 0x46,
G = 0x47,
H = 0x48,
I = 0x49,
J = 0x4A,
K = 0x4B,
L = 0x4C,
M = 0x4D,
N = 0x4E,
O = 0x4F,
P = 0x50,
Q = 0x51,
R = 0x52,
S = 0x53,
T = 0x54,
U = 0x55,
V = 0x56,
W = 0x57,
X = 0x58,
Y = 0x59,
Z = 0x5A,
// leave the rest of the ASCII range free
// in case we want to use it later
NonPrintable = 0x100,
Divide,
Multiply,
Subtract,
Plus,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
Home,
End,
Insert,
Delete,
PageUp,
PageDn,
Backspace,
Tab,
PrtScrn,
Pause,
Max,
}
}

View File

@@ -1,39 +0,0 @@
// ReSharper disable UnusedMember.Global
using System;
namespace Ryujinx.Graphics.RenderDocApi
{
[Flags]
public enum OverlayBits
{
/// <summary>
/// This single bit controls whether the overlay is enabled or disabled globally
/// </summary>
Enabled = 1 << 0,
/// <summary>
/// Show the average framerate over several seconds as well as min/max
/// </summary>
FrameRate = 1 << 1,
/// <summary>
/// Show the current frame number
/// </summary>
FrameNumber = 1 << 2,
/// <summary>
/// Show a list of recent captures, and how many captures have been made
/// </summary>
CaptureList = 1 << 3,
/// <summary>
/// Default values for the overlay mask
/// </summary>
Default = Enabled | FrameRate | FrameNumber | CaptureList,
/// <summary>
/// Enable all bits
/// </summary>
All = ~0,
/// <summary>
/// Disable all bits
/// </summary>
None = 0
}
}

View File

@@ -1,5 +0,0 @@
# Ryujinx.Graphics.RenderDocApi
This is a C# binding for RenderDoc's application API.
This is a source-inclusion of https://github.com/utkumaden/RenderdocSharp.
I didn't use the NuGet package as I had a few minor changes I wanted to make, and I want to learn from it as well via hands-on experience.

View File

@@ -1,639 +0,0 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
namespace Ryujinx.Graphics.RenderDocApi
{
public static unsafe partial class RenderDoc
{
/// <summary>
/// True if the API is available.
/// </summary>
public static bool IsAvailable => Api != null;
/// <summary>
/// Set the minimum version of the API you require.
/// </summary>
/// <remarks>Set this before you do anything else with the RenderDoc API, including <see cref="IsAvailable"/>.</remarks>
public static RenderDocVersion MinimumRequired { get; set; } = RenderDocVersion.Version_1_0_0;
/// <summary>
/// Set to true to assert versions.
/// </summary>
public static bool AssertVersionEnabled { get; set; } = true;
/// <summary>
/// Version of the API available.
/// </summary>
[MemberNotNullWhen(true, nameof(IsAvailable))]
public static Version? Version
{
get
{
if (!IsAvailable)
return null;
int major, minor, build;
Api->GetApiVersion(&major, &minor, &build);
return new Version(major, minor, build);
}
}
/// <summary>
/// The current mask which determines what sections of the overlay render on each window.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static OverlayBits OverlayBits
{
get => Api->GetOverlayBits();
set
{
Api->MaskOverlayBits(~value, value);
}
}
/// <summary>
/// The template for new captures.<br/>
/// The template can either be a relative or absolute path, which determines where captures will be saved and how they will be named.
/// If the path template is 'my_captures/example', then captures saved will be e.g.
/// 'my_captures/example_frame123.rdc' and 'my_captures/example_frame456.rdc'.<br/>
/// Relative paths will be saved relative to the processs current working directory.<br/>
/// </summary>
/// <remarks>The default template is in a folder controlled by the UI - initially the system temporary folder, and the filename is the executables filename.</remarks>
[RenderDocApiVersion(1, 0)]
public static string CaptureFilePathTemplate
{
get
{
byte* ptr = Api->GetCaptureFilePathTemplate();
return Marshal.PtrToStringUTF8((nint)ptr)!;
}
set
{
fixed (byte* ptr = value.ToNullTerminatedByteArray())
{
Api->SetCaptureFilePathTemplate(ptr);
}
}
}
/// <summary>
/// The amount of frame captures that have been made.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static int CaptureCount => Api->GetNumCaptures();
/// <summary>
/// Checks if the RenderDoc UI is currently connected to this process.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static bool IsTargetControlConnected => Api is not null && Api->IsTargetControlConnected() != 0;
/// <summary>
/// Checks if the current frame is capturing.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static bool IsFrameCapturing => Api is not null && Api->IsFrameCapturing() != 0;
/// <summary>
/// Set one of the options for tweaking some behaviors of capturing.
/// </summary>
/// <param name="option">specifies which capture option should be set.</param>
/// <param name="integer">the unsigned integer value to set for the option.</param>
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
/// <returns>
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool SetCaptureOption(CaptureOption option, uint integer)
{
return Api is not null && Api->SetCaptureOptionU32(option, integer) != 0;
}
/// <summary>
/// Set one of the options for tweaking some behaviors of capturing.
/// </summary>
/// <param name="option">specifies which capture option should be set.</param>
/// <param name="boolean">the value to set for the option, converted to a 0 or 1 before setting.</param>
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
/// <returns>
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool SetCaptureOption(CaptureOption option, bool boolean)
=> SetCaptureOption(option, boolean ? 1 : 0);
/// <summary>
/// Set one of the options for tweaking some behaviors of capturing.
/// </summary>
/// <param name="option">specifies which capture option should be set.</param>
/// <param name="single">the floating point value to set for the option.</param>
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
/// <returns>
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool SetCaptureOption(CaptureOption option, float single)
{
return Api is not null && Api->SetCaptureOptionF32(option, single) != 0;
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <param name="integer">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, <see cref="int.MaxValue"/>.</param>
[RenderDocApiVersion(1, 0)]
public static void GetCaptureOption(CaptureOption option, out uint integer)
{
integer = Api->GetCaptureOptionU32(option);
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <param name="single">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, -<see cref="float.MaxValue"/>.</param>
[RenderDocApiVersion(1, 0)]
public static void GetCaptureOption(CaptureOption option, out float single)
{
single = Api->GetCaptureOptionF32(option);
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>,
/// converted to a boolean.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <returns>
/// the value of the capture option, converted to bool, if the option is a valid <see cref="CaptureOption"/> enum.
/// Otherwise, returns null.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static bool? GetCaptureOptionBool(CaptureOption option)
{
if (Api is null) return false;
uint returnVal = GetCaptureOptionU32(option);
if (returnVal == uint.MaxValue)
return null;
return returnVal is not 0;
}
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <returns>
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
/// Otherwise, returns <see cref="int.MaxValue"/>.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static uint GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option);
/// <summary>
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
/// </summary>
/// <param name="option">specifies which capture option should be retrieved.</param>
/// <returns>
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
/// Otherwise, returns -<see cref="float.MaxValue"/>.
/// </returns>
[RenderDocApiVersion(1, 0)]
public static float GetCaptureOptionF32(CaptureOption option) => Api->GetCaptureOptionF32(option);
/// <summary>
/// Changes the key bindings in-application for changing the focussed window.
/// </summary>
/// <param name="buttons">lists the keys to bind.</param>
[RenderDocApiVersion(1, 0)]
public static void SetFocusToggleKeys(ReadOnlySpan<InputButton> buttons)
{
if (Api is null) return;
fixed (InputButton* ptr = buttons)
{
Api->SetFocusToggleKeys(ptr, buttons.Length);
}
}
/// <summary>
/// Changes the key bindings in-application for triggering a capture on the current window.
/// </summary>
/// <param name="buttons">lists the keys to bind.</param>
[RenderDocApiVersion(1, 0)]
public static void SetCaptureKeys(ReadOnlySpan<InputButton> buttons)
{
if (Api is null) return;
fixed (InputButton* ptr = buttons)
{
Api->SetCaptureKeys(ptr, buttons.Length);
}
}
/// <summary>
/// Attempts to remove RenderDoc and its hooks from the target process.<br/>
/// It must be called as early as possible in the process, and will have undefined results
/// if any graphics API functions have been called.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static void RemoveHooks()
{
if (Api is null) return;
Api->RemoveHooks();
}
/// <summary>
/// Remove RenderDocs crash handler from the target process.<br/>
/// If you have your own crash handler that you want to handle any exceptions,
/// RenderDocs handler could interfere; so it can be disabled.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static void UnloadCrashHandler()
{
if (Api is null) return;
Api->UnloadCrashHandler();
}
/// <summary>
/// Trigger a capture as if the user had pressed one of the capture hotkeys.<br/>
/// The capture will be taken from the next frame presented to whichever window is considered current.
/// </summary>
[RenderDocApiVersion(1, 0)]
public static void TriggerCapture()
{
if (Api is null) return;
Api->TriggerCapture();
}
/// <summary>
/// Gets the details of all frame capture in the current session.
/// This simply calls <see cref="GetCapture"/> for each index available as specified by <see cref="CaptureCount"/>.
/// </summary>
/// <returns>An immutable array of structs representing RenderDoc Captures.</returns>
public static ImmutableArray<Capture> GetCaptures()
{
if (Api is null) return [];
int captureCount = CaptureCount;
if (captureCount is 0) return [];
ImmutableArray<Capture>.Builder captures = ImmutableArray.CreateBuilder<Capture>(captureCount);
for (int captureIndex = 0; captureIndex < captureCount; captureIndex++)
{
if (GetCapture(captureIndex) is { } capture)
captures.Add(capture);
}
return captures.DrainToImmutable();
}
/// <summary>
/// Gets the details of a particular frame capture, as specified by an index from 0 to <see cref="CaptureCount"/> - 1.
/// </summary>
/// <param name="index">specifies which capture to return the details of. Must be less than the value returned by <see cref="CaptureCount"/>.</param>
/// <returns>A struct representing a RenderDoc Capture.</returns>
[RenderDocApiVersion(1, 0)]
public static Capture? GetCapture(int index)
{
if (Api is null) return null;
int length = 0;
if (Api->GetCapture(index, null, &length, null) == 0)
{
return null;
}
Span<byte> bytes = stackalloc byte[length + 1];
long timestamp;
fixed (byte* ptr = bytes)
Api->GetCapture(index, ptr, &length, &timestamp);
string fileName = Encoding.UTF8.GetString(bytes[length..]);
return new Capture(index, fileName, DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime);
}
/// <summary>
/// Determine the closest matching replay UI executable for the current RenderDoc module, and launch it.
/// </summary>
/// <param name="connectTargetControl">if the UI should immediately connect to the application.</param>
/// <param name="commandLine">string to be appended to the command line, e.g. a capture filename. If this parameter is null, the command line will be unmodified.</param>
/// <returns>true if the UI was successfully launched; false otherwise.</returns>
[RenderDocApiVersion(1, 0)]
public static bool LaunchReplayUI(bool connectTargetControl, string? commandLine = null)
{
if (Api is null) return false;
if (commandLine == null)
{
return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, null) != 0;
}
fixed (byte* ptr = commandLine.ToNullTerminatedByteArray())
{
return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, ptr) != 0;
}
}
/// <summary>
/// Explicitly sets which window is considered active.<br/>
/// The active window is the one that will be captured when the keybind to trigger a capture is pressed.
/// </summary>
/// <param name="hDevice">a handle to the API device object that will be set active. Must be valid.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. Must be valid.</param>
[RenderDocApiVersion(1, 0)]
public static void SetActiveWindow(nint hDevice, nint hWindow)
{
if (Api is null) return;
Api->SetActiveWindow((void*)hDevice, (void*)hWindow);
}
/// <summary>
/// Immediately begin a capture for the specified device/window combination.
/// </summary>
/// <param name="hDevice">a handle to the API device object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
[RenderDocApiVersion(1, 0)]
public static void StartFrameCapture(nint hDevice, nint hWindow)
{
if (Api is null) return;
Api->StartFrameCapture((void*)hDevice, (void*)hWindow);
}
/// <summary>
/// Immediately end an active capture for the specified device/window combination.
/// </summary>
/// <param name="hDevice">a handle to the API device object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <returns>true if the capture succeeded; false otherwise.</returns>
[RenderDocApiVersion(1, 0)]
public static bool EndFrameCapture(nint hDevice, nint hWindow)
{
if (Api is null) return false;
return Api->EndFrameCapture((void*)hDevice, (void*)hWindow) != 0;
}
/// <summary>
/// Trigger multiple sequential frame captures as if the user had pressed one of the capture hotkeys before each frame.<br/>
/// The captures will be taken from the next frames presented to whichever window is considered current.<br/>
/// Each capture will be taken independently and saved to a separate file, with no reference to the other frames.
/// </summary>
/// <param name="numFrames">the number of frames to capture.</param>
/// <remarks>Requires RenderDoc API version 1.1</remarks>
[RenderDocApiVersion(1, 1)]
public static void TriggerMultiFrameCapture(uint numFrames)
{
if (Api is null) return;
AssertAtLeast(1, 1);
Api->TriggerMultiFrameCapture(numFrames);
}
/// <summary>
/// Adds an arbitrary comments field to the most recent capture,
/// which will then be displayed in the UI to anyone opening the capture.
/// <br/><br/>
/// This is equivalent to calling <see cref="SetCaptureFileComments"/> with a null first (fileName) parameter.
/// </summary>
/// <param name="comments">the comments to set in the capture file.</param>
/// <remarks>Requires RenderDoc API version 1.2</remarks>
public static void SetMostRecentCaptureFileComments(string comments)
{
if (Api is null) return;
AssertAtLeast(1, 2);
byte[] commentBytes = comments.ToNullTerminatedByteArray();
fixed (byte* pcomment = commentBytes)
{
Api->SetCaptureFileComments((byte*)nint.Zero, pcomment);
}
}
/// <summary>
/// Adds an arbitrary comments field to an existing capture on disk,
/// which will then be displayed in the UI to anyone opening the capture.
/// </summary>
/// <param name="fileName">the path to the capture file to set comments in. If this path is null or an empty string, the most recent capture file that has been created will be used.</param>
/// <param name="comments">the comments to set in the capture file.</param>
/// <remarks>Requires RenderDoc API version 1.2</remarks>
[RenderDocApiVersion(1, 2)]
public static void SetCaptureFileComments(string? fileName, string comments)
{
if (Api is null) return;
AssertAtLeast(1, 2);
byte[] commentBytes = comments.ToNullTerminatedByteArray();
fixed (byte* pcomment = commentBytes)
{
if (fileName is null)
{
Api->SetCaptureFileComments((byte*)nint.Zero, pcomment);
}
else
{
byte[] fileBytes = fileName.ToNullTerminatedByteArray();
fixed (byte* pfile = fileBytes)
{
Api->SetCaptureFileComments(pfile, pcomment);
}
}
}
}
/// <summary>
/// Similar to <see cref="EndFrameCapture"/>, but the capture contents will be discarded immediately, and not processed and written to disk.<br/>
/// This will be more efficient than <see cref="EndFrameCapture"/> if the frame capture is not needed.
/// </summary>
/// <param name="hDevice">a handle to the API device object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
/// <returns>true if the capture was discarded; false if there was an error or no capture was in progress.</returns>
/// <remarks>Requires RenderDoc API version 1.4</remarks>
[RenderDocApiVersion(1, 4)]
public static bool DiscardFrameCapture(nint hDevice, nint hWindow)
{
if (Api is null) return false;
AssertAtLeast(1, 4);
return Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow) != 0;
}
/// <summary>
/// Requests that the currently connected replay UI raise its window to the top.<br/>
/// This is only possible if an instance of the replay UI is currently connected, otherwise this method does nothing.<br/>
/// This can be used in conjunction with <see cref="IsTargetControlConnected"/> and <see cref="LaunchReplayUI"/>,<br/> to intelligently handle showing the UI after making a capture.<br/><br/>
/// Given OS differences, it is not guaranteed that the UI will be successfully raised even if the request is passed on.<br/>
/// On some systems it may only be highlighted or otherwise indicated to the user.
/// </summary>
/// <returns>true if the request was passed onto the UI successfully; false if there is no UI connected or some other error occurred.</returns>
/// <remarks>Requires RenderDoc API version 1.5</remarks>
[RenderDocApiVersion(1, 5)]
public static bool ShowReplayUI()
{
if (Api is null) return false;
AssertAtLeast(1, 5);
return Api->ShowReplayUI() != 0;
}
/// <summary>
/// Sets a given title for the currently in-progress capture, which will be displayed in the UI.<br/>
/// This can be used either with a user-defined capture using a manual start and end,
/// or an automatic capture triggered by <see cref="TriggerCapture"/> or a keypress.<br/>
/// If multiple captures are ongoing at once, the title will be applied to the first capture to end only.<br/>
/// Any subsequent captures will not get any title unless the function is called again.
/// This function can only be called while a capture is in-progress,
/// after <see cref="StartFrameCapture"/> and before <see cref="EndFrameCapture"/>.<br/>
/// If it is called elsewhere it will have no effect.
/// If it is called multiple times within a capture, only the last title will have any effect.
/// </summary>
/// <param name="title">The title to set for the in-progress capture.</param>
/// <remarks>Requires RenderDoc API version 1.6</remarks>
[RenderDocApiVersion(1, 6)]
public static void SetCaptureTitle(string title)
{
if (Api is null) return;
AssertAtLeast(1, 6);
fixed (byte* ptr = title.ToNullTerminatedByteArray())
Api->SetCaptureTitle(ptr);
}
#region Dynamic Library loading
/// <summary>
/// Reload the internal RenderDoc API structure. Useful for manually refreshing <see cref="Api"/> while using process injection.
/// </summary>
/// <param name="ignoreAlreadyLoaded">Ignores the existing API function structure and overwrites it with a re-request.</param>
/// <param name="requiredVersion">The version of the RenderDoc API required by your application.</param>
public static void ReloadApi(bool ignoreAlreadyLoaded = false, RenderDocVersion? requiredVersion = null)
{
if (_loaded && !ignoreAlreadyLoaded)
return;
lock (typeof(RenderDoc))
{
// Prevent double loads.
if (_loaded && !ignoreAlreadyLoaded)
return;
if (requiredVersion.HasValue)
MinimumRequired = requiredVersion.Value;
_loaded = true;
_api = GetApi(MinimumRequired);
if (_api != null)
AssertAtLeast(MinimumRequired);
}
}
private static RenderDocApi* _api = null;
private static bool _loaded;
private static RenderDocApi* Api
{
get
{
ReloadApi();
return _api;
}
}
private static readonly Regex _dynamicLibraryPattern = RenderDocApiDynamicLibraryRegex();
private static RenderDocApi* GetApi(RenderDocVersion minimumRequired = RenderDocVersion.Version_1_0_0)
{
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
{
string moduleName = module.FileName ?? string.Empty;
if (!_dynamicLibraryPattern.IsMatch(moduleName))
continue;
if (!NativeLibrary.TryLoad(moduleName, out nint moduleHandle))
return null;
if (!NativeLibrary.TryGetExport(moduleHandle, "RENDERDOC_GetAPI", out nint procAddress))
return null;
var RENDERDOC_GetApi = (delegate* unmanaged[Cdecl]<RenderDocVersion, RenderDocApi**, int>)procAddress;
RenderDocApi* api;
return RENDERDOC_GetApi(minimumRequired, &api) != 0 ? api : null;
}
return null;
}
private static void AssertAtLeast(RenderDocVersion rdv, [CallerMemberName] string callee = "")
{
Version ver = rdv.SystemVersion;
AssertAtLeast(ver.Major, ver.Minor, ver.Build, callee);
}
private static void AssertAtLeast(int major, int minor, int patch = 0, [CallerMemberName] string callee = "")
{
if (!AssertVersionEnabled)
return;
if (Version!.Major < major)
goto fail;
if (Version.Major > major)
goto success;
if (Version.Minor < minor)
goto fail;
if (Version.Minor > minor)
goto success;
if (Version.Build < patch)
goto fail;
success:
return;
fail:
Version minVersion =
typeof(RenderDoc).GetMethod(callee)!.GetCustomAttribute<RenderDocApiVersionAttribute>()!.MinVersion;
throw new NotSupportedException(
$"This API was introduced in RenderDoc API {minVersion}. Current API version is {Version}.");
}
private static byte[] ToNullTerminatedByteArray(this string str, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
return encoding.GetBytes(str + '\0');
}
[GeneratedRegex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex RenderDocApiDynamicLibraryRegex();
#endregion
}
}

View File

@@ -1,51 +0,0 @@
namespace Ryujinx.Graphics.RenderDocApi
{
#pragma warning disable CS0649
internal unsafe struct RenderDocApi
{
public delegate* unmanaged[Cdecl]<int*, int*, int*, void> GetApiVersion;
public delegate* unmanaged[Cdecl]<CaptureOption, uint, int> SetCaptureOptionU32;
public delegate* unmanaged[Cdecl]<CaptureOption, float, int> SetCaptureOptionF32;
public delegate* unmanaged[Cdecl]<CaptureOption, uint> GetCaptureOptionU32;
public delegate* unmanaged[Cdecl]<CaptureOption, float> GetCaptureOptionF32;
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetFocusToggleKeys;
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetCaptureKeys;
public delegate* unmanaged[Cdecl]<OverlayBits> GetOverlayBits;
public delegate* unmanaged[Cdecl]<OverlayBits, OverlayBits, void> MaskOverlayBits;
public delegate* unmanaged[Cdecl]<void> RemoveHooks;
public delegate* unmanaged[Cdecl]<void> UnloadCrashHandler;
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureFilePathTemplate;
public delegate* unmanaged[Cdecl]<byte*> GetCaptureFilePathTemplate;
public delegate* unmanaged[Cdecl]<int> GetNumCaptures;
public delegate* unmanaged[Cdecl]<int, byte*, int*, long*, uint> GetCapture;
public delegate* unmanaged[Cdecl]<void> TriggerCapture;
public delegate* unmanaged[Cdecl]<uint> IsTargetControlConnected;
public delegate* unmanaged[Cdecl]<uint, byte*, uint> LaunchReplayUI;
public delegate* unmanaged[Cdecl]<void*, void*, void> SetActiveWindow;
public delegate* unmanaged[Cdecl]<void*, void*, void> StartFrameCapture;
public delegate* unmanaged[Cdecl]<uint> IsFrameCapturing;
public delegate* unmanaged[Cdecl]<void*, void*, uint> EndFrameCapture;
// 1.1
public delegate* unmanaged[Cdecl]<uint, void> TriggerMultiFrameCapture;
// 1.2
public delegate* unmanaged[Cdecl]<byte*, byte*, void> SetCaptureFileComments;
// 1.3
public delegate* unmanaged[Cdecl]<void*, void*, uint> DiscardFrameCapture;
// 1.5
public delegate* unmanaged[Cdecl]<uint> ShowReplayUI;
// 1.6
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureTitle;
}
#pragma warning restore CS0649
}

View File

@@ -1,16 +0,0 @@
using System;
namespace Ryujinx.Graphics.RenderDocApi
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
public sealed class RenderDocApiVersionAttribute : Attribute
{
public Version MinVersion { get; }
public RenderDocApiVersionAttribute(int major, int minor, int patch = 0)
{
MinVersion = new Version(major, minor, patch);
}
}
}

View File

@@ -1,47 +0,0 @@
using System;
namespace Ryujinx.Graphics.RenderDocApi
{
public enum RenderDocVersion
{
Version_1_0_0 = 10000,
Version_1_0_1 = 10001,
Version_1_0_2 = 10002,
Version_1_1_0 = 10100,
Version_1_1_1 = 10101,
Version_1_1_2 = 10102,
Version_1_2_0 = 10200,
Version_1_3_0 = 10300,
Version_1_4_0 = 10400,
Version_1_4_1 = 10401,
Version_1_4_2 = 10402,
Version_1_5_0 = 10500,
Version_1_6_0 = 10600,
}
public static partial class Helpers
{
extension(RenderDocVersion rdv)
{
public Version SystemVersion
{
get
{
int i = (int)rdv;
return new (i / 10000, (i % 10000) / 100, i % 100);
}
}
}
extension(Version sv)
{
public RenderDocVersion RenderDocVersion
{
get
{
return (RenderDocVersion)(sv.Major * 10000 + sv.Minor * 100 + sv.Build);
}
}
}
}
}

View File

@@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

View File

@@ -1,32 +0,0 @@
using Silk.NET.Vulkan;
using System.Runtime.CompilerServices;
namespace Ryujinx.Graphics.Vulkan
{
public static class Helpers
{
extension(Vk api)
{
/// <summary>
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
/// </summary>
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Vk"/>'s <see cref="Instance"/> pointer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void* GetRenderDocDevicePointer() =>
api.CurrentInstance is not null
? api.CurrentInstance.Value.GetRenderDocDevicePointer()
: null;
}
extension(Instance instance)
{
/// <summary>
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
/// </summary>
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Instance"/>'s pointer.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void* GetRenderDocDevicePointer()
=> (*((void**)(instance.Handle)));
}
}
}

View File

@@ -1,91 +1,43 @@
using Gommon;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Kernel.Memory;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace Ryujinx.HLE.Debugger
{
public partial class Debugger
{
private sealed record RcmdEntry(string[] Names, Func<Debugger, string, string> Handler, string[] HelpLines);
// Atmosphere/libraries/libmesosphere/source/kern_k_memory_block_manager.cpp
private static readonly string[] _memoryStateNames =
{
"----- Free -----",
"Io ",
"Static ",
"Code ",
"CodeData ",
"Normal ",
"Shared ",
"Alias ",
"AliasCode ",
"AliasCodeData ",
"Ipc ",
"Stack ",
"ThreadLocal ",
"Transfered ",
"SharedTransfered",
"SharedCode ",
"Inaccessible ",
"NonSecureIpc ",
"NonDeviceIpc ",
"Kernel ",
"GeneratedCode ",
"CodeOut ",
"Coverage ",
};
static Debugger()
{
_rcmdDelegates.Add(new RcmdEntry(
["help"],
(dbgr, _) => _rcmdDelegates
.Where(entry => entry.HelpLines.Length > 0)
.SelectMany(entry => entry.HelpLines)
.JoinToString('\n') + '\n',
Array.Empty<string>()));
_rcmdDelegates.Add(new RcmdEntry(["get info"], (dbgr, _) => dbgr.GetProcessInfo(), ["get info"]));
_rcmdDelegates.Add(new RcmdEntry(["backtrace", "bt"], (dbgr, _) => dbgr.GetStackTrace(), ["backtrace", "bt"]));
_rcmdDelegates.Add(new RcmdEntry(["registers", "reg"], (dbgr, _) => dbgr.GetRegisters(), ["registers", "reg"]));
_rcmdDelegates.Add(new RcmdEntry(["minidump"], (dbgr, _) => dbgr.GetMinidump(), ["minidump"]));
_rcmdDelegates.Add(new RcmdEntry(["get mappings"], (dbgr, args) => dbgr.GetMemoryMappings(args), ["get mappings", "get mappings {address}"]));
_rcmdDelegates.Add(new RcmdEntry(["get mapping"], (dbgr, args) => dbgr.GetMemoryMapping(args), ["get mapping {address}"]));
_rcmdDelegates.Add(["help"],
_ => _rcmdDelegates.Keys
.Where(x => !x[0].Equals("help"))
.Select(x => x.JoinToString('\n'))
.JoinToString('\n') + '\n'
);
_rcmdDelegates.Add(["get info"], dbgr => dbgr.GetProcessInfo());
_rcmdDelegates.Add(["backtrace", "bt"], dbgr => dbgr.GetStackTrace());
_rcmdDelegates.Add(["registers", "reg"], dbgr => dbgr.GetRegisters());
_rcmdDelegates.Add(["minidump"], dbgr => dbgr.GetMinidump());
}
private static readonly List<RcmdEntry> _rcmdDelegates = [];
private static readonly Dictionary<string[], Func<Debugger, string>> _rcmdDelegates = new();
public static string CallRcmdDelegate(Debugger debugger, string command)
public static Func<Debugger, string> FindRcmdDelegate(string command)
{
string originalCommand = command ?? string.Empty;
string trimmedCommand = originalCommand.Trim();
Func<Debugger, string> searchResult = _ => $"Unknown command: {command}\n";
foreach (RcmdEntry entry in _rcmdDelegates)
foreach ((string[] names, Func<Debugger, string> dlg) in _rcmdDelegates)
{
foreach (string name in entry.Names)
if (names.ContainsIgnoreCase(command.Trim()))
{
if (trimmedCommand.Equals(name, StringComparison.OrdinalIgnoreCase))
{
return entry.Handler(debugger, string.Empty);
}
if (trimmedCommand.Length > name.Length &&
trimmedCommand.StartsWith(name, StringComparison.OrdinalIgnoreCase) &&
char.IsWhiteSpace(trimmedCommand[name.Length]))
{
string arguments = trimmedCommand[name.Length..].TrimStart();
return entry.Handler(debugger, arguments);
}
searchResult = dlg;
break;
}
}
return $"Unknown command: {originalCommand}\n";
return searchResult;
}
public string GetStackTrace()
@@ -134,181 +86,5 @@ namespace Ryujinx.HLE.Debugger
return $"Error getting process info: {e.Message}\n";
}
}
public string GetMemoryMappings(string arguments)
{
if (Process?.MemoryManager is not { } memoryManager)
{
return "No application process found\n";
}
string trimmedArgs = arguments?.Trim() ?? string.Empty;
ulong startAddress = 0;
if (!string.IsNullOrEmpty(trimmedArgs))
{
if (!TryParseAddressArgument(trimmedArgs, out startAddress))
{
return $"Invalid address: {trimmedArgs}\n";
}
}
ulong requestedAddress = startAddress;
ulong currentAddress = Math.Max(requestedAddress, memoryManager.AddrSpaceStart);
StringBuilder sb = new();
sb.AppendLine($"Mappings (starting from 0x{requestedAddress:x10}):");
if (currentAddress >= memoryManager.AddrSpaceEnd)
{
return sb.ToString();
}
while (currentAddress < memoryManager.AddrSpaceEnd)
{
KMemoryInfo info = memoryManager.QueryMemory(currentAddress);
try
{
if (info.Size == 0 || info.Address >= memoryManager.AddrSpaceEnd)
{
break;
}
sb.AppendLine(FormatMapping(info, indent: true));
if (info.Address > ulong.MaxValue - info.Size)
{
break;
}
ulong nextAddress = info.Address + info.Size;
if (nextAddress <= currentAddress)
{
break;
}
currentAddress = nextAddress;
}
finally
{
KMemoryInfo.Pool.Release(info);
}
}
return sb.ToString();
}
public string GetMemoryMapping(string arguments)
{
if (Process?.MemoryManager is not { } memoryManager)
{
return "No application process found\n";
}
string trimmedArgs = arguments?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(trimmedArgs))
{
return "Missing address argument for `get mapping`\n";
}
if (!TryParseAddressArgument(trimmedArgs, out ulong address))
{
return $"Invalid address: {trimmedArgs}\n";
}
KMemoryInfo info = memoryManager.QueryMemory(address);
try
{
return FormatMapping(info, indent: false) + '\n';
}
finally
{
KMemoryInfo.Pool.Release(info);
}
}
private static string FormatMapping(KMemoryInfo info, bool indent)
{
ulong endAddress;
if (info.Size == 0)
{
endAddress = info.Address;
}
else if (info.Address > ulong.MaxValue - (info.Size - 1))
{
endAddress = ulong.MaxValue;
}
else
{
endAddress = info.Address + info.Size - 1;
}
string prefix = indent ? " " : string.Empty;
return $"{prefix}0x{info.Address:x10} - 0x{endAddress:x10} {GetPermissionString(info)} {GetMemoryStateName(info.State)} {GetAttributeFlags(info)} [{info.IpcRefCount}, {info.DeviceRefCount}]";
}
private static string GetPermissionString(KMemoryInfo info)
{
if ((info.State & MemoryState.UserMask) == MemoryState.Unmapped)
{
return " ";
}
return info.Permission switch
{
KMemoryPermission.ReadAndExecute => "r-x",
KMemoryPermission.Read => "r--",
KMemoryPermission.ReadAndWrite => "rw-",
_ => "---"
};
}
private static string GetMemoryStateName(MemoryState state)
{
int stateIndex = (int)(state & MemoryState.UserMask);
if ((uint)stateIndex < _memoryStateNames.Length)
{
return _memoryStateNames[stateIndex];
}
return "Unknown ";
}
private static bool TryParseAddressArgument(string text, out ulong value)
{
value = 0;
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
string trimmed = text.Trim();
if (trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
trimmed = trimmed[2..];
}
if (trimmed.Length == 0)
{
return false;
}
return ulong.TryParse(trimmed, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value);
}
private static string GetAttributeFlags(KMemoryInfo info)
{
char locked = info.Attribute.HasFlag(MemoryAttribute.Borrowed) ? 'L' : '-';
char ipc = info.Attribute.HasFlag(MemoryAttribute.IpcMapped) ? 'I' : '-';
char device = info.Attribute.HasFlag(MemoryAttribute.DeviceMapped) ? 'D' : '-';
char uncached = info.Attribute.HasFlag(MemoryAttribute.Uncached) ? 'U' : '-';
return $"{locked}{ipc}{device}{uncached}";
}
}
}

View File

@@ -404,8 +404,9 @@ namespace Ryujinx.HLE.Debugger.Gdb
string command = Helpers.FromHex(hexCommand);
Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}");
string response = Debugger.CallRcmdDelegate(Debugger, command);
Processor.ReplyHex(response);
Func<Debugger, string> rcmd = Debugger.FindRcmdDelegate(command);
Processor.ReplyHex(rcmd(Debugger));
}
catch (Exception e)
{

View File

@@ -2,7 +2,7 @@ using System;
namespace Ryujinx.HLE.Exceptions
{
class InvalidFirmwarePackageException : Exception
public class InvalidFirmwarePackageException : Exception
{
public InvalidFirmwarePackageException(string message) : base(message) { }
}

View File

@@ -487,7 +487,7 @@ namespace Ryujinx.HLE.FileSystem
if (keyPaths.Length is 0)
throw new FileNotFoundException($"Directory '{keysSource}' contained no '.keys' files.");
foreach (string filePath in keyPaths)
{
try
@@ -548,6 +548,9 @@ namespace Ryujinx.HLE.FileSystem
new DirectoryInfo(registeredDirectory).Delete(true);
}
if (!Directory.Exists(temporaryDirectory))
return; // nothing to move
Directory.Move(temporaryDirectory, registeredDirectory);
LoadEntries();

View File

@@ -219,6 +219,8 @@ namespace Ryujinx.HLE.FileSystem
FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig);
}
public bool HasKeySet { get; private set; }
public void ReloadKeySet()
{
KeySet ??= KeySet.CreateDefaultKeySet();
@@ -228,12 +230,19 @@ namespace Ryujinx.HLE.FileSystem
string consoleKeyFile = null;
string devKeyFile = null;
if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile)
{
LoadSetAtPath(AppDataManager.KeysDirPathUser);
}
LoadSetAtPath(AppDataManager.GetKeysDir());
LoadSetAtPath(AppDataManager.KeysDirPath);
HasKeySet = (prodKeyFile != null && titleKeyFile != null) || prodKeyFile != null;
ExternalKeyReader.ReadKeyFile(
KeySet,
prodKeyFile,
devKeyFile,
titleKeyFile,
consoleKeyFile);
return;
void LoadSetAtPath(string basePath)
{
@@ -262,8 +271,6 @@ namespace Ryujinx.HLE.FileSystem
devKeyFile = localDevKeyFile;
}
}
ExternalKeyReader.ReadKeyFile(KeySet, prodKeyFile, devKeyFile, titleKeyFile, consoleKeyFile, null);
}
public void ImportTickets(IFileSystem fs)

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.HLE.HOS.Services.Account.Acc
{
[JsonConverter(typeof(JsonStringEnumConverter<AccountState>))]
[JsonConverter(typeof(TypedStringEnumConverter<AccountState>))]
public enum AccountState
{
Closed,

View File

@@ -334,7 +334,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
private static string GetKeyRetailBinPath()
{
return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
return Path.Combine(AppDataManager.GetKeysDir(), "key_retail.bin");
}
public static bool HasAmiiboKeyFile => File.Exists(GetKeyRetailBinPath());

View File

@@ -1,7 +1,5 @@
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
@@ -12,34 +10,23 @@ namespace Ryujinx.UI.LocaleGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<AdditionalText> localeFiles = context.AdditionalTextsProvider.Where(static x => Path.GetDirectoryName(x.Path)?.Replace('\\', '/').EndsWith("assets/Locales") ?? false);
IncrementalValuesProvider<AdditionalText> localeFile = context.AdditionalTextsProvider.Where(static x => x.Path.EndsWith("locales.json"));
IncrementalValueProvider<ImmutableArray<(string, string)>> collectedContents = localeFiles.Select((text, cancellationToken) => (text.GetText(cancellationToken)!.ToString(), Path.GetFileName(text.Path))).Collect();
IncrementalValuesProvider<string> contents = localeFile.Select((text, cancellationToken) => text.GetText(cancellationToken)!.ToString());
context.RegisterSourceOutput(collectedContents, (spc, contents) =>
context.RegisterSourceOutput(contents, (spc, content) =>
{
IEnumerable<string> lines = content.Split('\n').Where(x => x.Trim().StartsWith("\"ID\":")).Select(x => x.Split(':')[1].Trim().Replace("\"", string.Empty).Replace(",", string.Empty));
StringBuilder enumSourceBuilder = new();
enumSourceBuilder.AppendLine("namespace Ryujinx.Ava.Common.Locale;");
enumSourceBuilder.AppendLine("public enum LocaleKeys");
enumSourceBuilder.AppendLine("{");
foreach ((string, string) content in contents)
foreach (string? line in lines)
{
IEnumerable<string> lines = content.Item1.Split('\n').Where(x => x.Trim().StartsWith("\"ID\":")).Select(x => x.Split(':')[1].Trim().Replace("\"", string.Empty).Replace(",", string.Empty));
foreach (string? line in lines)
{
if (content.Item2 == "Root.json")
{
enumSourceBuilder.AppendLine($" {line},");
}
else
{
enumSourceBuilder.AppendLine($" {content.Item2.Split('.')[0]}_{line},");
}
}
enumSourceBuilder.AppendLine($" {line},");
}
enumSourceBuilder.AppendLine("}");
spc.AddSource("LocaleKeys", enumSourceBuilder.ToString());

View File

@@ -264,7 +264,7 @@ namespace Ryujinx.Ava.Common
{
Dispatcher.UIThread.Post(waitingDialog.Close);
NotificationHelper.ShowInformation(
RyujinxNotificationManager.ShowInformation(
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
$"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
}
@@ -380,7 +380,7 @@ namespace Ryujinx.Ava.Common
{
Dispatcher.UIThread.Post(waitingDialog.Close);
NotificationHelper.ShowInformation(
RyujinxNotificationManager.ShowInformation(
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
$"{updateName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
}

View File

@@ -8,8 +8,6 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Common.Locale
@@ -40,6 +38,7 @@ namespace Ryujinx.Ava.Common.Locale
{ LocaleKeys.RyujinxConfirm, [RyujinxApp.FullAppName] },
{ LocaleKeys.RyujinxUpdater, [RyujinxApp.FullAppName] },
{ LocaleKeys.RyujinxRebooter, [RyujinxApp.FullAppName] },
{ LocaleKeys.SetupWizardGameDirsPageDescription, [RyujinxApp.FullAppName] },
{ LocaleKeys.CompatibilityListSearchBoxWatermarkWithCount, [CompatibilityDatabase.Entries.Length] },
{ LocaleKeys.CompatibilityListTitle, [CompatibilityDatabase.Entries.Length] }
});
@@ -160,86 +159,52 @@ namespace Ryujinx.Ava.Common.Locale
LocaleChanged?.Invoke();
}
private static LocalesData? _localeData;
private static LocalesJson? _localeData;
private static Dictionary<LocaleKeys, string> LoadJsonLanguage(string languageCode)
{
Dictionary<LocaleKeys, string> localeStrings = new();
if (_localeData is null)
_localeData ??= EmbeddedResources.ReadAllText("Ryujinx/Assets/Locale.json")
.Into(it => JsonHelper.Deserialize(it, LocalesJsonContext.Default.LocalesJson));
foreach (LocalesEntry locale in _localeData.Value.Locales)
{
Dictionary<string, LocalesJson> locales = [];
foreach (string uri in EmbeddedResources.GetAllAvailableResources("Ryujinx/Assets/Locales", ".json"))
if (locale.Translations.Count < _localeData.Value.Languages.Count)
{
string path = uri[..^".json".Length];
path = path.Replace('.', '/');
path = path.Append(".json");
locales.TryAdd(Path.GetFileName(path), EmbeddedResources.ReadAllText(path)
.Into(it => JsonHelper.Deserialize(it, LocalesJsonContext.Default.LocalesJson)));
throw new Exception(
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
_localeData = new LocalesData
if (locale.Translations.Count > _localeData.Value.Languages.Count)
{
Languages = EmbeddedResources.ReadAllText("Ryujinx/Assets/Languages.json")
.Into(it => JsonHelper.Deserialize(it, LanguagesJsonContext.Default.LanguagesJson)).Languages.Keys.ToList(),
LocalesFiles = locales
};
}
foreach ((string fileName, LocalesJson file) in _localeData.Value.LocalesFiles)
{
foreach (LocalesEntry locale in file.Locales)
{
if (locale.Translations.Count < _localeData.Value.Languages.Count)
{
throw new Exception(
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
if (locale.Translations.Count > _localeData.Value.Languages.Count)
{
throw new Exception(
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
if (!Enum.TryParse<LocaleKeys>(fileName == "Root.json" ? locale.ID : $"{fileName[..^".json".Length]}_{locale.ID}" , out LocaleKeys localeKey))
continue;
string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val)
? val
: locale.Translations[DefaultLanguageCode];
if (string.IsNullOrEmpty(str))
{
throw new Exception(
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
}
localeStrings[localeKey] = str;
throw new Exception(
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
}
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey))
continue;
string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val)
? val
: locale.Translations[DefaultLanguageCode];
if (string.IsNullOrEmpty(str))
{
throw new Exception(
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
}
localeStrings[localeKey] = str;
}
return localeStrings;
}
}
public struct LocalesData
{
public List<string> Languages { get; set; }
public Dictionary<string, LocalesJson> LocalesFiles { get; set; }
}
public struct LanguagesJson
{
public Dictionary<string, string> Languages { get; set; }
}
public struct LocalesJson
{
public List<string> Languages { get; set; }
public List<LocalesEntry> Locales { get; set; }
}
@@ -252,8 +217,4 @@ namespace Ryujinx.Ava.Common.Locale
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(LocalesJson))]
internal partial class LocalesJsonContext : JsonSerializerContext;
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(LanguagesJson))]
internal partial class LanguagesJsonContext : JsonSerializerContext;
}

View File

@@ -0,0 +1,32 @@
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Gommon;
using System;
namespace Ryujinx.Ava.Common
{
// ReSharper disable once InconsistentNaming
// UiImages is ugly, so no
public static class UIImages
{
public const string LogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}_{1}.png?assembly=Ryujinx";
public const string IconPathFormat = "resm:Ryujinx.Assets.UIImages.Icon_{0}.png?assembly=Ryujinx";
public static Bitmap LoadBitmap(string uri)
=> new(AssetLoader.Open(new Uri(uri)));
public static Bitmap GetIconByName(string iconName)
=> LoadBitmap(IconPathFormat.Format(iconName));
public static Bitmap GetLogoByNameAndTheme(string iconName, bool isDarkTheme) =>
LoadBitmap(LogoPathFormat.Format(iconName,
isDarkTheme
? "Dark"
: "Light"
)
);
public static Bitmap GetLogoByNameAndVariant(string iconName, string theme)
=> LoadBitmap(LogoPathFormat.Format(iconName, theme));
}
}

View File

@@ -156,12 +156,9 @@ namespace Ryujinx.Headless
option.UserProfile = profile.Name;
// Check if keys exists.
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
if (!File.Exists(Path.Combine(AppDataManager.GetKeysDir(), "prod.keys")))
{
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
{
Logger.Error?.Print(LogClass.Application, "Keys not found");
}
Logger.Error?.Print(LogClass.Application, "Keys not found");
}
ReloadConfig();

View File

@@ -18,7 +18,6 @@ using Ryujinx.Common.GraphicsDriver;
using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInterop;
using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless;
using Ryujinx.SDL3.Common;
@@ -33,6 +32,8 @@ namespace Ryujinx.Ava
{
internal static class Program
{
public static bool IsFirstStart { get; set; }
public static double WindowScaleFactor { get; set; }
public static double DesktopScaleFactor { get; set; } = 1.0;
public static string Version { get; private set; }
@@ -188,12 +189,9 @@ namespace Ryujinx.Ava
DriverUtilities.InitDriverConfig(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
// Check if keys exists.
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
if (!File.Exists(Path.Combine(AppDataManager.GetKeysDir(), "prod.keys")))
{
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
{
MainWindow.ShowKeyErrorOnLoad = true;
}
MainWindow.ShowKeyErrorOnLoad = true;
}
if (CommandLineState.LaunchPathArg != null)
@@ -222,7 +220,6 @@ namespace Ryujinx.Ava
public static void ReloadConfig(bool isRunGameWithCustomConfig = false)
{
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
@@ -248,6 +245,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.LoadDefault();
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
IsFirstStart = true;
}
else
{
@@ -263,6 +261,8 @@ namespace Ryujinx.Ava
ConfigurationFileFormat.RenameInvalidConfigFile(ConfigurationPath);
IsFirstStart = true;
ConfigurationState.Instance.LoadDefault();
}
}

View File

@@ -78,9 +78,7 @@
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
@@ -88,6 +86,7 @@
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
<ProjectReference Include="..\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
@@ -135,7 +134,7 @@
</ItemGroup>
<ItemGroup>
<None Remove="Assets\**\*.json" />
<None Remove="Assets\locales.json" />
<None Remove="Assets\Styles\Styles.xaml" />
<None Remove="Assets\Styles\Themes.xaml" />
<None Remove="Assets\Icons\Controller_JoyConLeft.svg" />
@@ -157,8 +156,8 @@
<EmbeddedResource Include="..\..\docs\compatibility.csv" LogicalName="RyujinxGameCompatibilityList">
<Link>Assets\RyujinxGameCompatibility.csv</Link>
</EmbeddedResource>
<EmbeddedResource Include="..\..\assets\**\*.json">
<LinkBase>Assets</LinkBase>
<EmbeddedResource Include="..\..\assets\locales.json">
<Link>Assets\Locale.json</Link>
</EmbeddedResource>
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
@@ -179,6 +178,6 @@
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\assets\Locales\*.json" />
<AdditionalFiles Include="..\..\assets\locales.json" />
</ItemGroup>
</Project>

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration
{
[JsonConverter(typeof(JsonStringEnumConverter<AudioBackend>))]
[JsonConverter(typeof(TypedStringEnumConverter<AudioBackend>))]
public enum AudioBackend
{
Dummy,

View File

@@ -1,9 +1,10 @@
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.SystemState;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.System
{
[JsonConverter(typeof(JsonStringEnumConverter<Language>))]
[JsonConverter(typeof(TypedStringEnumConverter<Language>))]
public enum Language
{
Japanese,

View File

@@ -1,9 +1,10 @@
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.SystemState;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.System
{
[JsonConverter(typeof(JsonStringEnumConverter<Region>))]
[JsonConverter(typeof(TypedStringEnumConverter<Region>))]
public enum Region
{
Japan,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.UI
{
[JsonConverter(typeof(JsonStringEnumConverter<FocusLostType>))]
[JsonConverter(typeof(TypedStringEnumConverter<FocusLostType>))]
public enum FocusLostType
{
DoNothing,

View File

@@ -1,8 +1,9 @@
using Ryujinx.Common.Utilities;
using System.Text.Json.Serialization;
namespace Ryujinx.Ava.Systems.Configuration.UI
{
[JsonConverter(typeof(JsonStringEnumConverter<UpdaterType>))]
[JsonConverter(typeof(TypedStringEnumConverter<UpdaterType>))]
public enum UpdaterType
{
Off,

View File

@@ -1,9 +1,18 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
namespace Ryujinx.Ava.UI.Helpers
{
public static class ControlExtensions
{
public static RyujinxNotificationManager CreateNotificationManager(
this Window window,
NotificationPosition visiblePosition = NotificationPosition.BottomRight,
int maxItems = RyujinxNotificationManager.MaxNotifications,
Thickness? margin = null
) => new(window, visiblePosition, maxItems, margin);
extension(Control ctrl)
{
public int GridRow

View File

@@ -1,107 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common;
using System;
using System.Collections.Concurrent;
using System.Threading;
namespace Ryujinx.Ava.UI.Helpers
{
public static class NotificationHelper
{
private const int MaxNotifications = 4;
private const int NotificationDelayInMs = 5000;
private static WindowNotificationManager _notificationManager;
private static readonly BlockingCollection<Notification> _notifications = new();
public static void SetNotificationManager(Window host)
{
_notificationManager = new WindowNotificationManager(host)
{
Position = NotificationPosition.BottomRight,
MaxItems = MaxNotifications,
Margin = new Thickness(0, 0, 15, 40),
};
Lazy<AsyncWorkQueue<Notification>> maybeAsyncWorkQueue = new(
() => new AsyncWorkQueue<Notification>(notification =>
{
Dispatcher.UIThread.Post(() =>
{
_notificationManager.Show(notification);
});
},
"UI.NotificationThread",
_notifications),
LazyThreadSafetyMode.ExecutionAndPublication);
_notificationManager.TemplateApplied += (sender, args) =>
{
// NOTE: Force creation of the AsyncWorkQueue.
_ = maybeAsyncWorkQueue.Value;
};
host.Closing += (sender, args) =>
{
if (maybeAsyncWorkQueue.IsValueCreated)
{
maybeAsyncWorkQueue.Value.Dispose();
}
};
}
public static void Show(string title, string text, NotificationType type, bool waitingExit = false, Action onClick = null, Action onClose = null)
{
TimeSpan delay = waitingExit ? TimeSpan.FromMilliseconds(0) : TimeSpan.FromMilliseconds(NotificationDelayInMs);
_notifications.Add(new Notification(title, text, type, delay, onClick, onClose));
}
public static void ShowError(string message) =>
ShowError(
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}"
);
public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
Show(
title,
text,
NotificationType.Information,
waitingExit,
onClick,
onClose);
public static void ShowSuccess(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
Show(
title,
text,
NotificationType.Success,
waitingExit,
onClick,
onClose);
public static void ShowWarning(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
Show(
title,
text,
NotificationType.Warning,
waitingExit,
onClick,
onClose);
public static void ShowError(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) =>
Show(
title,
text,
NotificationType.Error,
waitingExit,
onClick,
onClose);
}
}

View File

@@ -0,0 +1,179 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common;
using System;
using System.Collections.Concurrent;
using System.Threading;
namespace Ryujinx.Ava.UI.Helpers
{
public class RyujinxNotificationManager
{
public static RyujinxNotificationManager Shared { get; set; }
public const int MaxNotifications = 4;
private const int NotificationDelayInMs = 5000;
private readonly WindowNotificationManager _notificationManager;
private readonly BlockingCollection<Notification> _notifications = new();
public RyujinxNotificationManager(Window host,
NotificationPosition visiblePosition = NotificationPosition.BottomRight,
int maxItems = MaxNotifications,
Thickness? margin = null)
{
_notificationManager = new WindowNotificationManager(host)
{
Position = visiblePosition,
MaxItems = maxItems,
Margin = margin ?? new Thickness(0, 0, 15, 40)
};
Lazy<AsyncWorkQueue<Notification>> maybeAsyncWorkQueue = new(
() => new AsyncWorkQueue<Notification>(notification =>
{
Dispatcher.UIThread.Post(() =>
{
_notificationManager.Show(notification);
});
},
"UI.NotificationThread",
_notifications),
LazyThreadSafetyMode.ExecutionAndPublication);
_notificationManager.TemplateApplied += (sender, args) =>
{
// NOTE: Force creation of the AsyncWorkQueue.
_ = maybeAsyncWorkQueue.Value;
};
host.Closing += (sender, args) =>
{
if (maybeAsyncWorkQueue.IsValueCreated)
{
maybeAsyncWorkQueue.Value.Dispose();
}
};
}
public static void Show(string title, string text, NotificationType type, bool waitingExit = false,
Action onClick = null, Action onClose = null)
=> Shared?.Send(title, text, type, waitingExit, onClick, onClose);
public void Send(string title, string text, NotificationType type, bool waitingExit = false,
Action onClick = null, Action onClose = null)
{
TimeSpan delay = waitingExit
? TimeSpan.FromMilliseconds(0)
: TimeSpan.FromMilliseconds(NotificationDelayInMs);
_notifications.Add(new Notification(title, text, type, delay, onClick, onClose));
}
#region Instance notification senders
public void Information(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Send(
title,
text,
NotificationType.Information,
waitingExit,
onClick,
onClose);
public void Success(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Send(
title,
text,
NotificationType.Success,
waitingExit,
onClick,
onClose);
public void Warning(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Send(
title,
text,
NotificationType.Warning,
waitingExit,
onClick,
onClose);
public void Error(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Send(
title,
text,
NotificationType.Error,
waitingExit,
onClick,
onClose);
public void Error(string message, bool waitingExit = false) =>
Error(
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}",
waitingExit: waitingExit
);
#endregion
#region Static notification senders
public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Show(
title,
text,
NotificationType.Information,
waitingExit,
onClick,
onClose);
public static void ShowSuccess(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Show(
title,
text,
NotificationType.Success,
waitingExit,
onClick,
onClose);
public static void ShowWarning(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Show(
title,
text,
NotificationType.Warning,
waitingExit,
onClick,
onClose);
public static void ShowError(string title, string text, bool waitingExit = false, Action onClick = null,
Action onClose = null) =>
Show(
title,
text,
NotificationType.Error,
waitingExit,
onClick,
onClose);
public static void ShowError(string message, bool waitingExit = false) =>
ShowError(
LocaleManager.Instance[LocaleKeys.DialogErrorTitle],
$"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}",
waitingExit: waitingExit
);
#endregion
}
}

View File

@@ -0,0 +1,172 @@
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ncm;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
using SkiaSharp;
using System;
using System.Buffers.Binary;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Ryujinx.Ava.UI.Models
{
public class FirmwareAvatarCache : BaseModel, IReadOnlyDictionary<string, byte[]>
{
private readonly Dictionary<string, byte[]> _backing = new();
public FirmwareAvatarCache(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
{
string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data);
string avatarPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath);
if (!string.IsNullOrWhiteSpace(avatarPath))
{
using IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open);
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
{
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs"))
{
using UniqueRef<IFile> file = new();
romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
using MemoryStream streamPng = new();
file.Get.AsStream().CopyTo(stream);
stream.Position = 0;
SKImage avatarImage = SKImage.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream));
using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100))
{
data.SaveTo(streamPng);
}
_backing[item.FullPath] = streamPng.ToArray();
}
}
}
}
public IEnumerable<ProfileImageModel> CreateProfileImageModels()
=> this.Select(x => new ProfileImageModel(x.Key, x.Value));
private static byte[] DecompressYaz0(MemoryStream stream)
{
using BinaryReader reader = new(stream);
reader.ReadInt32(); // Magic
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
reader.ReadInt64(); // Padding
byte[] input = new byte[stream.Length - stream.Position];
stream.ReadExactly(input, 0, input.Length);
uint inputOffset = 0;
byte[] output = new byte[decodedLength];
uint outputOffset = 0;
ushort mask = 0;
byte header = 0;
while (outputOffset < decodedLength)
{
if ((mask >>= 1) == 0)
{
header = input[inputOffset++];
mask = 0x80;
}
if ((header & mask) != 0)
{
if (outputOffset == output.Length)
{
break;
}
output[outputOffset++] = input[inputOffset++];
}
else
{
byte byte1 = input[inputOffset++];
byte byte2 = input[inputOffset++];
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
uint position = outputOffset - (dist + 1);
uint length = (uint)byte1 >> 4;
if (length == 0)
{
length = (uint)input[inputOffset++] + 0x12;
}
else
{
length += 2;
}
uint gap = outputOffset - position;
uint nonOverlappingLength = length;
if (nonOverlappingLength > gap)
{
nonOverlappingLength = gap;
}
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
outputOffset += nonOverlappingLength;
position += nonOverlappingLength;
length -= nonOverlappingLength;
while (length-- > 0)
{
output[outputOffset++] = output[position++];
}
}
}
return output;
}
#region dictionary impl
IEnumerator<KeyValuePair<string, byte[]>> IEnumerable<KeyValuePair<string, byte[]>>.GetEnumerator()
{
return (_backing as IEnumerable<KeyValuePair<string, byte[]>>).GetEnumerator();
}
public IEnumerator GetEnumerator()
{
return ((IEnumerable)_backing).GetEnumerator();
}
public int Count => _backing.Count;
public bool ContainsKey(string key) => _backing.ContainsKey(key);
public bool TryGetValue(string key, out byte[] value) => _backing.TryGetValue(key, out value);
public byte[] this[string key] => _backing[key];
public IEnumerable<string> Keys => _backing.Keys;
public IEnumerable<byte[]> Values => _backing.Values;
#endregion
}
}

View File

@@ -2,12 +2,8 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.HLE;
using SPB.Graphics;
using SPB.Platform;
using SPB.Platform.GLX;
@@ -34,7 +30,6 @@ namespace Ryujinx.Ava.UI.Renderer
protected nint MetalLayer { get; set; }
public delegate void UpdateBoundsCallbackDelegate(Rect rect);
private UpdateBoundsCallbackDelegate _updateBoundsCallback;
public event EventHandler<nint> WindowCreated;
@@ -51,55 +46,6 @@ namespace Ryujinx.Ava.UI.Renderer
protected virtual void OnWindowDestroyed() { }
public bool ToggleRenderDocCapture(Switch device)
{
if (!RenderDoc.IsAvailable) return false;
if (RenderDoc.IsFrameCapturing)
{
if (EndRenderDocCapture())
{
Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture.");
return true;
}
}
else if (StartRenderDocCapture(device))
{
Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture.");
return true;
}
return false;
}
public bool StartRenderDocCapture(Switch device)
{
if (!RenderDoc.IsAvailable) return false;
if (RenderDoc.IsFrameCapturing) return false;
RenderDoc.StartFrameCapture(nint.Zero, WindowHandle);
RenderDoc.SetCaptureTitle(TitleHelper.FormatRenderDocCaptureTitle(device.Processes.ActiveApplication, Program.Version));
return true;
}
public bool EndRenderDocCapture()
{
if (!RenderDoc.IsAvailable) return false;
if (!RenderDoc.IsFrameCapturing) return false;
return RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle);
}
public bool DiscardRenderDocCapture()
{
if (!RenderDoc.IsAvailable) return false;
if (!RenderDoc.IsFrameCapturing) return false;
return RenderDoc.IsFrameCapturing && RenderDoc.DiscardFrameCapture(nint.Zero, WindowHandle);
}
protected virtual void OnWindowDestroying()
{
WindowHandle = nint.Zero;
@@ -178,9 +124,7 @@ namespace Ryujinx.Ava.UI.Renderer
}
else
{
X11Window = PlatformHelper.CreateOpenGLWindow(
new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100,
100) as GLXWindow;
X11Window = PlatformHelper.CreateOpenGLWindow(new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100, 100) as GLXWindow;
}
WindowHandle = X11Window.WindowHandle.RawHandle;
@@ -194,7 +138,7 @@ namespace Ryujinx.Ava.UI.Renderer
{
_className = "NativeWindow-" + Guid.NewGuid();
_wndProcDelegate = delegate(nint hWnd, WindowsMessages msg, nint wParam, nint lParam)
_wndProcDelegate = delegate (nint hWnd, WindowsMessages msg, nint wParam, nint lParam)
{
switch (msg)
{
@@ -217,8 +161,7 @@ namespace Ryujinx.Ava.UI.Renderer
RegisterClassEx(ref wndClassEx);
WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480,
control.Handle, nint.Zero, nint.Zero, nint.Zero);
WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480, control.Handle, nint.Zero, nint.Zero, nint.Zero);
SetWindowLongPtrW(control.Handle, GWLP_WNDPROC, wndClassEx.lpfnWndProc);

View File

@@ -15,7 +15,7 @@ using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.Ava.UI.SetupWizard;
using System;
using System.Diagnostics;
@@ -57,8 +57,6 @@ namespace Ryujinx.Ava
if (OperatingSystem.IsMacOS())
{
// Switches macOS key held behavior to repeat the input key instead of showing the character accents menu (like doing on an iOS keyboard would).
// https://macos-defaults.com/keyboard/applepressandholdenabled.html
Process.Start("/usr/bin/defaults", "write org.ryujinx.Ryujinx ApplePressAndHoldEnabled -bool false");
}
}

View File

@@ -0,0 +1,40 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:pages="clr-namespace:Ryujinx.Ava.UI.SetupWizard.Pages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="pages:SetupFinishedPageContext"
x:Class="Ryujinx.Ava.UI.SetupWizard.Pages.SetupFinishedPage">
<Grid
ColumnDefinitions="*"
RowDefinitions="*,Auto"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
<Border
Margin="15"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CornerRadius="5"
Background="{DynamicResource AppListBackgroundColor}">
<TextBlock Margin="15" Text="{ext:Locale SetupWizardFinalPageDescription}" TextAlignment="Center" TextWrapping="Wrap" />
</Border>
<Button Grid.Row="1"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
MinWidth="45"
MinHeight="32"
Padding="8"
Background="Transparent"
Click="Button_OnClick"
CornerRadius="5"
Tag="https://discord.gg/PEuzjrFXUA"
ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}">
<StackPanel Orientation="Horizontal" Spacing="5">
<Image Source="{Binding OwningWizard.DiscordLogo}" />
<TextBlock Text="Discord"/>
</StackPanel>
</Button>
</Grid>
</UserControl>

View File

@@ -0,0 +1,22 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Common.Helper;
namespace Ryujinx.Ava.UI.SetupWizard.Pages
{
public partial class SetupFinishedPage : RyujinxControl<SetupFinishedPageContext>
{
public SetupFinishedPage()
{
InitializeComponent();
}
private void Button_OnClick(object sender, RoutedEventArgs e)
{
if (sender is Button { Tag: string url })
OpenHelper.OpenUrl(url);
}
}
}

View File

@@ -0,0 +1,13 @@
using Gommon;
using Ryujinx.Ava.Common.Locale;
namespace Ryujinx.Ava.UI.SetupWizard.Pages
{
public class SetupFinishedPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardFinalPageTitle)
{
public override LocaleKeys ActionContent => LocaleKeys.SetupWizardFinalPageAction;
// informative step; this implementation is not called.
public override Result CompleteStep() => Result.Success;
}
}

View File

@@ -0,0 +1,27 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:pages="clr-namespace:Ryujinx.Ava.UI.SetupWizard.Pages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="pages:SetupFirmwarePageContext"
x:Class="Ryujinx.Ava.UI.SetupWizard.Pages.SetupFirmwarePage">
<StackPanel>
<TextBlock Text="{ext:Locale SetupWizardFirmwarePageDescription}"/>
<Grid ColumnDefinitions="*" RowDefinitions="*,Auto">
<TextBox Name="FirmwarePathField" Margin="0, 10, 0, 5" Text="{Binding FirmwareSourcePath}" IsReadOnly="True" />
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="3.5">
<Button
Content="{ext:Locale SetupWizardFirmwarePageFolderBrowse}"
Command="{Binding BrowseFolderCommand}"
CommandParameter="{Binding #FirmwarePathField}"/>
<Button
Content="{ext:Locale SetupWizardFirmwarePageFileBrowse}"
Command="{Binding BrowseFileCommand}"
CommandParameter="{Binding #FirmwarePathField}"/>
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,12 @@
using Ryujinx.Ava.UI.Controls;
namespace Ryujinx.Ava.UI.SetupWizard.Pages
{
public partial class SetupFirmwarePage : RyujinxControl<SetupFirmwarePageContext>
{
public SetupFirmwarePage()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,167 @@
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.HLE.FileSystem;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard.Pages
{
public partial class SetupFirmwarePageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardFirmwarePageTitle)
{
[ObservableProperty] public partial string FirmwareSourcePath { get; set; }
[RelayCommand]
private static async Task BrowseFile(TextBox tb)
{
Optional<IStorageFile> result =
await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFilePickerAsync(
new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageFilePopupTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.FileDialogAllTypes])
{
Patterns = ["*.xci", "*.zip"],
AppleUniformTypeIdentifiers = ["com.ryujinx.xci", "public.zip-archive"],
MimeTypes = ["application/x-nx-xci", "application/zip"],
},
new("XCI")
{
Patterns = ["*.xci"],
AppleUniformTypeIdentifiers = ["com.ryujinx.xci"],
MimeTypes = ["application/x-nx-xci"],
},
new("ZIP")
{
Patterns = ["*.zip"],
AppleUniformTypeIdentifiers = ["public.zip-archive"],
MimeTypes = ["application/zip"],
}
}
});
if (result.TryGet(out IStorageFile firmwareFile))
{
tb.Text = firmwareFile.TryGetLocalPath();
}
}
[RelayCommand]
private static async Task BrowseFolder(TextBox tb)
{
Optional<IStorageFolder> result =
await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(
new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageFolderPopupTitle]
});
if (result.TryGet(out IStorageFolder firmwareFolder))
{
tb.Text = firmwareFolder.TryGetLocalPath();
}
}
public override object CreateHelpContent()
{
Grid grid = new()
{
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
HorizontalAlignment = HorizontalAlignment.Center
};
grid.Children.Add(new TextBlock
{
Text = LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageHelpText],
HorizontalAlignment = HorizontalAlignment.Center,
GridRow = 0
});
grid.Children.Add(new HyperlinkButton
{
Content = LocaleManager.Instance[LocaleKeys.SetupWizardHelpLinkButton],
NavigateUri = new Uri(SharedConstants.DumpFirmwareWikiUrl),
HorizontalAlignment = HorizontalAlignment.Center,
GridRow = 1
});
return grid;
}
public override Result CompleteStep()
{
if (string.IsNullOrEmpty(FirmwareSourcePath) && RyujinxSetupWizard.HasFirmware)
{
NotificationManager.Information(
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
text: LocaleManager.GetFormatted(
LocaleKeys.SetupWizardFirmwarePageSkipText,
LocaleManager.Instance[LocaleKeys.SetupWizardActionBack]
)
);
return Result.Success; // This handles the user selecting no file/dir and just hitting Next.
}
if (!Directory.Exists(FirmwareSourcePath))
return Result.Fail;
try
{
RyujinxApp.MainWindow.ContentManager.InstallFirmware(FirmwareSourcePath);
SystemVersion installedFwVer = RyujinxApp.MainWindow.ContentManager.GetCurrentFirmwareVersion();
if (installedFwVer != null)
{
NotificationManager.Information(
LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageInstallSuccessNotificationTitle],
LocaleManager.GetFormatted(
LocaleKeys.SetupWizardFirmwarePageInstallSuccessNotificationTitle,
installedFwVer.VersionString
)
);
}
else
{
NotificationManager.Error(
LocaleManager.Instance[LocaleKeys.SetupWizardFirmwarePageInstallFailNotificationTitle],
LocaleManager.GetFormatted(
LocaleKeys.SetupWizardFirmwarePageInstallFailNotificationText,
FirmwareSourcePath
)
);
}
RyujinxApp.MainWindow.ViewModel.RefreshFirmwareStatus(installedFwVer, allowNullVersion: true);
// Purge Applet Cache.
DirectoryInfo miiEditorCacheFolder = new(
Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache")
);
if (miiEditorCacheFolder.Exists)
{
miiEditorCacheFolder.Delete(true);
}
}
catch (Exception e)
{
NotificationManager.Error(e.Message, waitingExit: true);
return Result.Fail;
}
return Result.Success;
}
}
}

View File

@@ -0,0 +1,107 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:pages="clr-namespace:Ryujinx.UI.SetupWizard.Pages"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.UI.SetupWizard.Pages.SetupGameDirsPage"
x:DataType="pages:SetupGameDirsPageContext">
<StackPanel
Margin="10"
Spacing="10"
Orientation="Vertical" HorizontalAlignment="Stretch">
<TextBlock Foreground="{DynamicResource SecondaryTextColor}" Text="{ext:Locale SetupWizardGameDirsPageDescription}" />
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabGeneralGameDirectories}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<ListBox
Name="GameDirsList"
MinHeight="120"
ItemsSource="{Binding GameDirs}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="10" />
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
</Style>
</ListBox.Styles>
</ListBox>
<Grid HorizontalAlignment="Stretch" ColumnDefinitions="*,Auto,Auto">
<TextBox
Name="GameDirPathBox"
Margin="0"
Watermark="{ext:Locale AddGameDirBoxTooltip}"
VerticalAlignment="Stretch" />
<Button
Name="AddGameDirButton"
Grid.Column="1"
MinWidth="90"
Margin="10,0,0,0">
<TextBlock HorizontalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralAdd}" />
</Button>
<Button
Name="RemoveGameDirButton"
Grid.Column="2"
MinWidth="90"
Margin="5,0,0,0"
Click="RemoveGameDirButton_OnClick">
<TextBlock HorizontalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralRemove}" />
</Button>
</Grid>
</StackPanel>
<Separator Height="1" />
<StackPanel Orientation="Vertical" Spacing="5">
<StackPanel Orientation="Horizontal">
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabGeneralAutoloadDirectories}" />
</StackPanel>
<TextBlock Foreground="{DynamicResource SecondaryTextColor}"
Text="{ext:Locale SettingsTabGeneralAutoloadNote}" />
</StackPanel>
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<ListBox
Name="AutoloadDirsList"
MinHeight="100"
ItemsSource="{Binding UpdateAndDlcDirs}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="10" />
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
</Style>
</ListBox.Styles>
</ListBox>
<Grid HorizontalAlignment="Stretch" ColumnDefinitions="*,Auto,Auto">
<TextBox
Name="AutoloadDirPathBox"
Margin="0"
Watermark="{ext:Locale AddGameDirBoxTooltip}"
VerticalAlignment="Stretch" />
<Button
Name="AddAutoloadDirButton"
Grid.Column="1"
MinWidth="90"
Margin="10,0,0,0">
<TextBlock HorizontalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralAdd}" />
</Button>
<Button
Name="RemoveAutoloadDirButton"
Grid.Column="2"
MinWidth="90"
Margin="5,0,0,0"
Click="RemoveAutoloadDirButton_OnClick">
<TextBlock HorizontalAlignment="Center"
Text="{ext:Locale SettingsTabGeneralRemove}" />
</Button>
</Grid>
</StackPanel>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,80 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Ryujinx.Ava;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Utilities;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.UI.SetupWizard.Pages
{
public partial class SetupGameDirsPage : RyujinxControl<SetupGameDirsPageContext>
{
public SetupGameDirsPage()
{
InitializeComponent();
AddGameDirButton.Command =
Commands.Create(() => AddDirButton(GameDirPathBox, ViewModel.GameDirs));
AddAutoloadDirButton.Command =
Commands.Create(() => AddDirButton(AutoloadDirPathBox, ViewModel.UpdateAndDlcDirs));
}
private async Task AddDirButton(TextBox addDirBox, ObservableCollection<string> directories)
{
string path = addDirBox.Text;
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !directories.Contains(path))
{
directories.Add(path);
addDirBox.Clear();
}
else
{
Gommon.Optional<IStorageFolder> folder = await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync();
if (folder.HasValue)
{
directories.Add(folder.Value.Path.LocalPath);
}
}
}
private void RemoveGameDirButton_OnClick(object sender, RoutedEventArgs e)
{
int oldIndex = GameDirsList.SelectedIndex;
foreach (string path in new List<string>(GameDirsList.SelectedItems.Cast<string>()))
{
ViewModel.GameDirs.Remove(path);
}
if (GameDirsList.ItemCount > 0)
{
GameDirsList.SelectedIndex = oldIndex < GameDirsList.ItemCount ? oldIndex : 0;
}
}
private void RemoveAutoloadDirButton_OnClick(object sender, RoutedEventArgs e)
{
int oldIndex = AutoloadDirsList.SelectedIndex;
foreach (string path in new List<string>(AutoloadDirsList.SelectedItems.Cast<string>()))
{
ViewModel.UpdateAndDlcDirs.Remove(path);
}
if (AutoloadDirsList.ItemCount > 0)
{
AutoloadDirsList.SelectedIndex = oldIndex < AutoloadDirsList.ItemCount ? oldIndex : 0;
}
}
}
}

View File

@@ -0,0 +1,71 @@
using Avalonia.Controls;
using Avalonia.Layout;
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
using Gommon;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.SetupWizard;
using Ryujinx.Common;
using System;
using System.Linq;
namespace Ryujinx.UI.SetupWizard.Pages
{
public partial class SetupGameDirsPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardGameDirsPageTitle)
{
[ObservableProperty]
public partial ObservableCollection<string> GameDirs { get; set; }
= new(ConfigurationState.Instance.UI.GameDirs);
[ObservableProperty]
public partial ObservableCollection<string> UpdateAndDlcDirs { get; set; }
= new(ConfigurationState.Instance.UI.AutoloadDirs);
public override Result CompleteStep()
{
if (GameDirs.Count is 0)
{
NotificationManager.Error(LocaleManager.Instance[LocaleKeys.SetupWizardGameDirsPageNoFoldersSelectedError]);
return Result.Fail;
}
OwningWizard.ModifyConfig(config =>
{
config.UI.GameDirs.Value = GameDirs.ToList();
config.UI.AutoloadDirs.Value = UpdateAndDlcDirs.ToList();
});
RyujinxApp.MainWindow.LoadApplications();
return Result.Success;
}
public override object CreateHelpContent()
{
Grid grid = new()
{
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
HorizontalAlignment = HorizontalAlignment.Center
};
grid.Children.Add(new TextBlock
{
Text = LocaleManager.Instance[LocaleKeys.SetupWizardGameDirsPageHelpText],
HorizontalAlignment = HorizontalAlignment.Center,
GridRow = 0
});
grid.Children.Add(new HyperlinkButton
{
Content = LocaleManager.Instance[LocaleKeys.SetupWizardHelpLinkButton],
HorizontalAlignment = HorizontalAlignment.Center,
NavigateUri = new Uri(SharedConstants.DumpContentWikiUrl),
GridRow = 1
});
return grid;
}
}
}

View File

@@ -0,0 +1,22 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="clr-namespace:Ryujinx.Ava.UI.SetupWizard.Pages"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.SetupWizard.Pages.SetupKeysPage"
x:DataType="pages:SetupKeysPageContext">
<StackPanel>
<TextBlock Text="{ext:Locale SetupWizardKeysPageDescription}" Margin="0,0,0,10"/>
<Grid ColumnDefinitions="*,Auto">
<TextBox Name="KeysFolderPathField" Text="{Binding KeysFolderPath}" IsReadOnly="True" />
<Button Grid.Column="1"
Content="..."
Command="{Binding BrowseCommand}"
CommandParameter="{Binding #KeysFolderPathField}"
Margin="5,0,0,0"/>
</Grid>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Ryujinx.Ava.UI.Controls;
namespace Ryujinx.Ava.UI.SetupWizard.Pages
{
public partial class SetupKeysPage : RyujinxControl<SetupKeysPageContext>
{
public SetupKeysPage()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,130 @@
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DynamicData;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Utilities;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.FileSystem;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard.Pages
{
public partial class SetupKeysPageContext() : SetupWizardPageContext(LocaleKeys.SetupWizardKeysPageTitle)
{
public override object CreateHelpContent()
{
Grid grid = new()
{
RowDefinitions = [new(GridLength.Auto), new(GridLength.Auto)],
HorizontalAlignment = HorizontalAlignment.Center
};
grid.Children.Add(new TextBlock
{
Text = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageHelpText],
HorizontalAlignment = HorizontalAlignment.Center,
GridRow = 0
});
grid.Children.Add(new HyperlinkButton
{
Content = LocaleManager.Instance[LocaleKeys.SetupWizardHelpLinkButton],
HorizontalAlignment = HorizontalAlignment.Center,
NavigateUri = new Uri(SharedConstants.DumpKeysWikiUrl),
GridRow = 1
});
return grid;
}
[ObservableProperty] public partial string KeysFolderPath { get; set; }
[RelayCommand]
private static async Task Browse(TextBox tb)
{
Optional<IStorageFolder> result =
await RyujinxApp.MainWindow.ViewModel.StorageProvider.OpenSingleFolderPickerAsync(
new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.SetupWizardKeysPageFolderPopupTitle]
});
if (result.TryGet(out IStorageFolder keyFolder))
{
tb.Text = keyFolder.TryGetLocalPath();
}
}
public override Result CompleteStep()
{
if (string.IsNullOrEmpty(KeysFolderPath) && RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
{
NotificationManager.Information(
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
text: LocaleManager.GetFormatted(
LocaleKeys.SetupWizardKeysPageSkipText,
LocaleManager.Instance[LocaleKeys.SetupWizardActionBack]
));
return Result.Success; // This handles the user selecting no folder and just hitting Next.
}
if (!Directory.Exists(KeysFolderPath))
return Result.Fail;
try
{
Logger.Info?.Print(LogClass.Application, $"Installing keys from {KeysFolderPath}");
ContentManager.InstallKeys(KeysFolderPath, AppDataManager.GetKeysDir());
NotificationManager.Information(
title: LocaleManager.Instance[LocaleKeys.RyujinxInfo],
text: LocaleManager.Instance[LocaleKeys.DialogKeysInstallerKeysInstallSuccessMessage]);
}
catch (InvalidFirmwarePackageException ifwpe)
{
NotificationManager.Error(ifwpe.Message, waitingExit: true);
return Result.Failure(NoKeysFoundInFolder.Shared);
}
catch (MissingKeyException ex)
{
NotificationManager.Error(ex.ToString(), waitingExit: true);
return Result.Failure(NoKeysFoundInFolder.Shared);
}
catch (Exception ex)
{
string message = ex.Message;
if (ex is FormatException)
{
message = LocaleManager.Instance.UpdateAndGetDynamicValue(
LocaleKeys.DialogKeysInstallerKeysNotFoundErrorMessage, KeysFolderPath);
}
NotificationManager.Error(message, waitingExit: true);
return Result.Failure(new MessageError(message));
}
finally
{
RyujinxApp.MainWindow.VirtualFileSystem.ReloadKeySet();
}
return Result.Success;
}
}
public struct NoKeysFoundInFolder : IErrorState
{
public static readonly NoKeysFoundInFolder Shared = new();
}
}

View File

@@ -0,0 +1,3 @@
# Ryubing Setup Wizard
Directly modified from the code found [here](https://github.com/TKMM-Team/Tkmm/tree/master/src/Tkmm/Wizard).

View File

@@ -0,0 +1,82 @@
using Ryujinx.Ava.UI.SetupWizard.Pages;
using Ryujinx.UI.SetupWizard.Pages;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard
{
public partial class RyujinxSetupWizard
{
private async ValueTask<bool> SetupKeys()
{
if (_overwrite || !RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
{
Retry:
bool result = await NextPage<SetupKeysPage, SetupKeysPageContext>(out SetupKeysPageContext keyContext)
.Show();
if (!result)
return false;
if (!keyContext.CompleteStep())
goto Retry;
}
return true;
}
private async ValueTask<bool> SetupFirmware()
{
if (_overwrite || !HasFirmware)
{
if (!RyujinxApp.MainWindow.VirtualFileSystem.HasKeySet)
{
NotificationManager.Error("Keys still seem to not be installed. Please try again.");
return false;
}
Retry:
bool result =
await NextPage<SetupFirmwarePage, SetupFirmwarePageContext>(out SetupFirmwarePageContext fwContext)
.Show();
if (!result)
return false;
if (!fwContext.CompleteStep())
goto Retry;
OnPropertyChanged(nameof(HasFirmware));
}
return true;
}
private async ValueTask<bool> SetupGameDirs()
{
if (!HasFirmware)
{
NotificationManager.Error("Firmware still seems to not be installed. Please try again.");
return false;
}
Retry:
bool result =
await NextPage<SetupGameDirsPage, SetupGameDirsPageContext>(out SetupGameDirsPageContext gdContext)
.Show();
if (!result)
return false;
if (!gdContext.CompleteStep())
goto Retry;
return true;
}
private ValueTask<bool> Finish()
=> NextPage<SetupFinishedPage, SetupFinishedPageContext>(out _)
.WithHelpButtonVisible(false)
.Show();
}
}

View File

@@ -0,0 +1,144 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Media.Imaging;
using Avalonia.Styling;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using System;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard
{
public partial class RyujinxSetupWizard : BaseModel, IDisposable
{
private bool _configWasModified;
private readonly RyujinxSetupWizardWindow _window;
private readonly bool _overwrite;
public void SetWindowTitle(string titleText)
{
_window.Title = titleText;
ToolTip.SetTip(_window.RyuLogo, titleText);
}
public RyujinxSetupWizard(RyujinxSetupWizardWindow wizardWindow, bool overwriteMode)
{
_window = wizardWindow;
_overwrite = overwriteMode;
if (Program.PreviewerDetached)
{
UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle);
RyujinxApp.ThemeChanged += Ryujinx_ThemeChanged;
}
else
{
UpdateLogoTheme("Dark");
}
}
private SetupWizardPage FirstPage() => new(_window.WizardPresenter, this, isFirstPage: true);
private SetupWizardPage NextPage() => new(_window.WizardPresenter, this);
private SetupWizardPage NextPage<TControl, TContext>(out TContext boundContext)
where TControl : RyujinxControl<TContext>, new()
where TContext : SetupWizardPageContext, new()
=> NextPage()
.WithContent<TControl, TContext>(out boundContext)
.WithTitle(boundContext.Title)
.WithActionContent(boundContext.ActionContent);
public static bool HasFirmware => RyujinxApp.MainWindow.ContentManager.GetCurrentFirmwareVersion() != null;
public RyujinxNotificationManager NotificationManager { get; private set; }
internal void ModifyConfig(Action<ConfigurationState> modifier)
{
modifier(ConfigurationState.Instance);
_configWasModified = true;
}
public async Task Start()
{
NotificationManager = _window.CreateNotificationManager(
// I wanted to do bottom center but that...literally just shows top center? Okay.
// Fuck it, weird window height hack to do it instead.
// 120 is not exact, just a random number. Looks fine though.
NotificationPosition.TopCenter,
margin: new Thickness(0, _window.Height - 135, 0, 0)
);
RyujinxSetupWizardWindow.IsOpen = true;
Start:
await FirstPage()
.WithTitle(LocaleKeys.SetupWizardFirstPageTitle)
.WithContent(LocaleKeys.SetupWizardFirstPageContent)
.WithActionContent(LocaleKeys.SetupWizardFirstPageAction)
.Show();
// result is unhandled as the first page cannot display anything other than the next button.
// back does not need to be handled
Keys:
if (!await SetupKeys())
goto Start;
Firmware:
if (!await SetupFirmware())
goto Keys;
GameDirs:
if (!await SetupGameDirs())
goto Firmware;
if (!await Finish())
goto GameDirs;
Return:
if (_configWasModified)
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
NotificationManager = null;
_window.Close();
RyujinxSetupWizardWindow.IsOpen = false;
}
#region Discord logo stuff
[ObservableProperty] public partial Bitmap DiscordLogo { get; set; }
private void Ryujinx_ThemeChanged()
{
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle));
}
private void UpdateLogoTheme(string theme)
{
bool isDarkTheme = theme == "Dark" ||
(theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark);
DiscordLogo = UIImages
.GetLogoByNameAndTheme("Discord", isDarkTheme)
.CreateScaledBitmap(new PixelSize(32, 24));
}
public void Dispose()
{
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
DiscordLogo.Dispose();
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@@ -0,0 +1,18 @@
<windows:StyleableAppWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:windows="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:setupWizard="clr-namespace:Ryujinx.Ava.UI.SetupWizard"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
CanResize="False"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.SetupWizard.RyujinxSetupWizardWindow"
x:DataType="setupWizard:RyujinxSetupWizard">
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" RowDefinitions="Auto,*">
<Grid Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Name="FlushControls">
<controls:RyujinxLogo Name="RyuLogo"/>
</Grid>
<ContentPresenter Grid.Row="1" Name="WizardPresenter"/>
</Grid>
</windows:StyleableAppWindow>

View File

@@ -0,0 +1,90 @@
using Avalonia.Controls;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.IO;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard
{
public partial class RyujinxSetupWizardWindow : StyleableAppWindow
{
public static bool IsOpen { get; set; }
public RyujinxSetupWizardWindow() : base(useCustomTitleBar: true)
{
InitializeComponent();
if (Program.PreviewerDetached)
{
FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI;
}
}
public static Task ShowAsync(bool overwriteMode, Window owner = null)
{
if (!CanShowSetupWizard)
return Task.CompletedTask;
Task windowTask = ShowAsync(
CreateWindow(out RyujinxSetupWizard wiz, overwriteMode),
owner
);
_ = wiz.Start();
return windowTask.ContinueWith(_ => wiz.Dispose());
}
public static RyujinxSetupWizardWindow CreateWindow(out RyujinxSetupWizard setupWizard, bool overwriteMode = false)
{
RyujinxSetupWizardWindow window = new();
window.DataContext = setupWizard = new RyujinxSetupWizard(window, overwriteMode);
window.Height = 700;
window.Width = 825;
return window;
}
public static bool CanShowSetupWizard =>
!File.Exists(Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard"));
public static bool DisableSetupWizard()
{
if (!CanShowSetupWizard)
return false; //cannot disable; file exists, so it's already disabled.
string disableFile = Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard");
try
{
File.Create(disableFile, 0).Dispose();
File.SetAttributes(disableFile, File.GetAttributes(disableFile) | FileAttributes.Hidden);
return true;
}
catch (Exception e)
{
Logger.Error?.PrintStack(LogClass.Application, e.Message);
return false;
}
}
public static bool EnableSetupWizard()
{
if (CanShowSetupWizard)
return false; //cannot enable; file does not exist, so it's already enabled.
string disableFile = Path.Combine(AppDataManager.BaseDirPath, ".DoNotShowSetupWizard");
try
{
File.Delete(disableFile);
return true;
}
catch (Exception e)
{
Logger.Error?.PrintStack(LogClass.Application, e.Message);
return false;
}
}
}
}

View File

@@ -0,0 +1,94 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
namespace Ryujinx.Ava.UI.SetupWizard
{
public partial class SetupWizardPage
{
public SetupWizardPage WithTitle(LocaleKeys title) => WithTitle(LocaleManager.Instance[title]);
public SetupWizardPage WithTitle(string title)
{
Title = title;
return this;
}
public SetupWizardPage WithContent(LocaleKeys content) => WithContent(LocaleManager.Instance[content]);
public SetupWizardPage WithContent(object? content)
{
if (content is StyledElement { Parent: ContentControl parent })
{
parent.Content = null;
}
Content = content;
return this;
}
public SetupWizardPage WithHelpContent(LocaleKeys content) =>
WithHelpContent(LocaleManager.Instance[content]);
public SetupWizardPage WithHelpContent(object? content)
{
if (content is string str)
{
TextBlock tb = new()
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap,
FontSize = 20.0,
Text = str
};
tb.Classes.Add("h1");
content = tb;
}
HelpContent = content;
HasHelpContent = content != null;
return this;
}
public SetupWizardPage WithContent<TControl>(object? context = null) where TControl : Control, new()
{
Content = new TControl { DataContext = context };
return this;
}
public SetupWizardPage WithContent<TControl, TContext>(out TContext boundContext)
where TControl : RyujinxControl<TContext>, new()
where TContext : SetupWizardPageContext, new()
{
boundContext = new() { OwningWizard = ownerWizard };
if (boundContext.CreateHelpContent() is { } content)
WithHelpContent(content);
return WithContent<TControl>(boundContext);
}
public SetupWizardPage WithActionContent(LocaleKeys content) =>
WithActionContent(LocaleManager.Instance[content]);
public SetupWizardPage WithActionContent(object? content)
{
ActionContent = content;
return this;
}
public SetupWizardPage WithHelpButtonVisible(bool visible)
{
ShowHelpButton = visible;
return this;
}
}
}

View File

@@ -0,0 +1,67 @@
using Avalonia.Controls.Presenters;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.SetupWizard
{
public partial class SetupWizardPage(
ContentPresenter contentPresenter,
RyujinxSetupWizard ownerWizard,
bool isFirstPage = false) : BaseModel
{
private bool? _result;
private readonly CancellationTokenSource _cts = new();
public bool IsFirstPage => isFirstPage;
public RyujinxSetupWizard Parent => ownerWizard;
[ObservableProperty] public partial string? Title { get; set; }
[ObservableProperty] public partial object? Content { get; set; }
[ObservableProperty] public partial object? HelpContent { get; set; }
[ObservableProperty] public partial bool HasHelpContent { get; set; }
[ObservableProperty] public partial bool ShowHelpButton { get; set; } = true;
[ObservableProperty]
public partial object? ActionContent { get; set; } = LocaleManager.Instance[LocaleKeys.SetupWizardActionNext];
[RelayCommand]
private void MoveBack()
{
_result = false;
_cts.Cancel();
}
[RelayCommand]
private void MoveNext()
{
_result = true;
_cts.Cancel();
}
public async ValueTask<bool> Show()
{
contentPresenter.Content = new SetupWizardPageView { ViewModel = this };
ownerWizard.SetWindowTitle(Title);
try
{
await Task.Delay(-1, _cts.Token);
}
catch (TaskCanceledException)
{
return _result ?? false;
}
return false;
}
}
}

View File

@@ -0,0 +1,35 @@
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
namespace Ryujinx.Ava.UI.SetupWizard
{
public abstract class SetupWizardPageContext(LocaleKeys title) : BaseModel
{
public RyujinxSetupWizard OwningWizard
{
get;
init
{
field = value;
NotificationManager = field.NotificationManager;
}
}
public RyujinxNotificationManager NotificationManager { get; private init; }
public LocaleKeys Title => title;
public virtual LocaleKeys ActionContent => LocaleKeys.SetupWizardActionNext;
// ReSharper disable once UnusedMemberInSuper.Global
// it's used implicitly as we use this type as a where guard for generics for WithContent<TControl, TContext>,
// it also ensures all context types implement completion
public abstract Result CompleteStep();
#nullable enable
public virtual object? CreateHelpContent() => null;
}
}

View File

@@ -0,0 +1,92 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:fa="using:Projektanker.Icons.Avalonia"
xmlns:wiz="using:Ryujinx.Ava.UI.SetupWizard"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="wiz:SetupWizardPage"
x:Class="Ryujinx.Ava.UI.SetupWizard.SetupWizardPageView">
<Grid RowDefinitions="*,Auto" Margin="60">
<ScrollViewer>
<Grid RowDefinitions="Auto,*">
<TextBlock Grid.Row="0"
TextWrapping="WrapWithOverflow"
FontSize="46"
Text="{Binding Title}" />
<ContentPresenter Grid.Row="1"
Content="{Binding}"
IsVisible="{Binding !#InfoToggle.IsChecked}"
TextWrapping="WrapWithOverflow"
Margin="0,15,0,0">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="{x:Type wiz:SetupWizardPage}">
<ContentControl Content="{Binding Content}" VerticalAlignment="Stretch"/>
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
<Grid Grid.Row="1"
ColumnDefinitions="*" RowDefinitions="*,Auto"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
IsVisible="{Binding #InfoToggle.IsChecked}">
<Border
Margin="15"
IsVisible="{Binding HasHelpContent}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="5"
Background="{DynamicResource AppListBackgroundColor}">
<ContentPresenter Content="{Binding}"
Margin="5"
TextWrapping="WrapWithOverflow" VerticalAlignment="Center" HorizontalAlignment="Center">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="{x:Type wiz:SetupWizardPage}">
<ContentControl Content="{Binding HelpContent}" />
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
</Border>
<Button Grid.Row="1"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
MinWidth="45"
MinHeight="32"
MaxWidth="45"
MaxHeight="32"
Padding="8"
Background="Transparent"
Click="Button_OnClick"
CornerRadius="5"
Tag="https://discord.gg/PEuzjrFXUA"
ToolTip.Tip="{ext:Locale AboutDiscordUrlTooltipMessage}">
<Image Source="{Binding Parent.DiscordLogo}" />
</Button>
</Grid>
</Grid>
</ScrollViewer>
<Grid ColumnDefinitions="Auto,Auto,*" Grid.Row="1">
<ToggleButton Name="InfoToggle" Padding="6" IsVisible="{Binding ShowHelpButton}">
<fa:Icon Value="fa-solid fa-circle-info" />
</ToggleButton>
<Button IsVisible="{Binding !IsFirstPage}"
Grid.Column="1"
Content="{ext:Locale SetupWizardActionBack}"
Margin="10,0,0,0"
Command="{Binding MoveBackCommand}" />
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Grid.Column="2">
<Button Content="{Binding ActionContent}"
Command="{Binding MoveNextCommand}" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,23 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Common.Helper;
namespace Ryujinx.Ava.UI.SetupWizard
{
public partial class SetupWizardPageView : RyujinxControl<SetupWizardPage>
{
public SetupWizardPageView()
{
InitializeComponent();
}
private void Button_OnClick(object sender, RoutedEventArgs e)
{
if (sender is Button { Tag: string url })
OpenHelper.OpenUrl(url);
}
}
}

View File

@@ -1,8 +1,9 @@
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Styling;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using System;
@@ -36,21 +37,17 @@ namespace Ryujinx.Ava.UI.ViewModels
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value));
}
private const string LogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}_{1}.png?assembly=Ryujinx";
private void UpdateLogoTheme(string theme)
{
bool isDarkTheme = theme == "Dark" ||
(theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark);
string themeName = isDarkTheme ? "Dark" : "Light";
DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
GitLabLogo = LoadBitmap(LogoPathFormat.Format("GitLab", themeName));
DiscordLogo = UIImages.GetLogoByNameAndTheme("Discord", isDarkTheme)
.CreateScaledBitmap(new PixelSize(32, 24));
GitLabLogo = UIImages.GetLogoByNameAndTheme("GitLab", isDarkTheme)
.CreateScaledBitmap(new PixelSize(32, 31));
}
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
public void Dispose()
{
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;

View File

@@ -38,7 +38,6 @@ using Ryujinx.Common.Logging;
using Ryujinx.Common.UI;
using Ryujinx.Common.Utilities;
using Ryujinx.Cpu;
using Ryujinx.Graphics.RenderDocApi;
using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
@@ -46,6 +45,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.UI;
using Ryujinx.Input.HLE;
using Ryujinx.Ava.UI.SetupWizard;
using SkiaSharp;
using System;
using System.Collections.Generic;
@@ -105,7 +105,7 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] public partial Brush ProgressBarForegroundColor { get; set; }
[ObservableProperty] public partial Brush ProgressBarBackgroundColor { get; set; }
#pragma warning disable MVVMTK0042 // Must stay a normal observable field declaration since this is used as an out parameter target
[ObservableProperty] private ReadOnlyObservableCollection<ApplicationData> _appsObservableList;
#pragma warning restore MVVMTK0042
@@ -130,7 +130,8 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] public partial string LastScannedAmiiboId { get; set; }
[ObservableProperty] public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64;
[ObservableProperty]
public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64;
[ObservableProperty] public partial bool ShowContent { get; set; } = true;
[ObservableProperty] public partial float VolumeBeforeMute { get; set; }
@@ -870,7 +871,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void RefreshGrid()
{
IObservableList<ApplicationData> appsList = Applications.ToObservableChangeSet()
_ = Applications.ToObservableChangeSet()
.Filter(Filter)
.Sort(GetComparer())
.Bind(out ReadOnlyObservableCollection<ApplicationData> apps)
@@ -1013,16 +1014,11 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
private async Task HandleKeysInstallation(string filename)
public async Task HandleKeysInstallation(string filename)
{
try
{
string systemDirectory = AppDataManager.KeysDirPath;
if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile &&
Directory.Exists(AppDataManager.KeysDirPathUser))
{
systemDirectory = AppDataManager.KeysDirPathUser;
}
string systemDirectory = AppDataManager.GetKeysDir();
string dialogTitle =
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallTitle);
@@ -1376,8 +1372,8 @@ namespace Ryujinx.Ava.UI.ViewModels
Patterns = ["*.zip"],
AppleUniformTypeIdentifiers = ["public.zip-archive"],
MimeTypes = ["application/zip"],
},
},
}
}
});
if (result.HasValue)
@@ -1757,12 +1753,19 @@ namespace Ryujinx.Ava.UI.ViewModels
public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime));
public void RefreshFirmwareStatus()
/// <remarks>
/// By default, this method will try to retrieve the installed FW version if the version parameter is null.
/// <paramref name="allowNullVersion"/> forces this method to accept null and not re-lookup
/// in the case you want to deliberately cause an update with a missing firmware version;
///
/// i.e., in the setup wizard.
/// </remarks>
public void RefreshFirmwareStatus(SystemVersion version = null, bool allowNullVersion = false)
{
SystemVersion version = null;
try
{
version = ContentManager.GetCurrentFirmwareVersion();
if (!allowNullVersion)
version ??= ContentManager.GetCurrentFirmwareVersion();
}
catch (Exception)
{
@@ -1865,29 +1868,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public void ReloadRenderDocApi()
{
RenderDoc.ReloadApi(ignoreAlreadyLoaded: true);
OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton), nameof(RenderDocIsAvailable));
if (RenderDoc.IsAvailable)
RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
NotificationHelper.ShowInformation(
"RenderDoc API reloaded",
RenderDoc.IsAvailable ? "RenderDoc is now available." : "RenderDoc is no longer available."
);
}
public void ToggleCapture()
{
if (ShowLoadProgress) return;
AppHost.RendererHost.EmbeddedWindow.ToggleRenderDocCapture(AppHost.Device);
RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
}
public void ToggleFullscreen()
{
if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs)
@@ -1970,16 +1950,15 @@ namespace Ryujinx.Ava.UI.ViewModels
{
if (ConfigurationState.Instance.Debug.DebuggerSuspendOnStart)
{
NotificationHelper.ShowInformation(
RyujinxNotificationManager.ShowInformation(
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckSuspendOnStartTitle],
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckSuspendOnStartMessage]);
}
if (ConfigurationState.Instance.Debug.EnableGdbStub)
{
NotificationHelper.ShowInformation(
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle,
ConfigurationState.Instance.Debug.GdbStubPort.Value),
RyujinxNotificationManager.ShowInformation(
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, ConfigurationState.Instance.Debug.GdbStubPort.Value),
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckGdbStubMessage]);
}
@@ -1988,22 +1967,20 @@ namespace Ryujinx.Ava.UI.ViewModels
var memoryConfigurationLocaleKey = ConfigurationState.Instance.System.DramSize.Value switch
{
MemoryConfiguration.MemoryConfiguration4GiB or
MemoryConfiguration.MemoryConfiguration4GiBAppletDev or
MemoryConfiguration.MemoryConfiguration4GiBSystemDev =>
LocaleKeys.SettingsTabSystemDramSize4GiB,
MemoryConfiguration.MemoryConfiguration4GiBAppletDev or
MemoryConfiguration.MemoryConfiguration4GiBSystemDev => LocaleKeys.SettingsTabSystemDramSize4GiB,
MemoryConfiguration.MemoryConfiguration6GiB or
MemoryConfiguration.MemoryConfiguration6GiBAppletDev =>
LocaleKeys.SettingsTabSystemDramSize6GiB,
MemoryConfiguration.MemoryConfiguration6GiBAppletDev => LocaleKeys.SettingsTabSystemDramSize6GiB,
MemoryConfiguration.MemoryConfiguration8GiB => LocaleKeys.SettingsTabSystemDramSize8GiB,
MemoryConfiguration.MemoryConfiguration12GiB => LocaleKeys.SettingsTabSystemDramSize12GiB,
_ => LocaleKeys.SettingsTabSystemDramSize4GiB,
};
NotificationHelper.ShowWarning(
RyujinxNotificationManager.ShowWarning(
LocaleManager.Instance.UpdateAndGetDynamicValue(
LocaleKeys.NotificationLaunchCheckDramSizeTitle,
LocaleKeys.NotificationLaunchCheckDramSizeTitle,
LocaleManager.Instance[memoryConfigurationLocaleKey]
),
),
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckDramSizeMessage]);
}
}
@@ -2488,67 +2465,6 @@ namespace Ryujinx.Ava.UI.ViewModels
png.SaveTo(fileStream);
});
public bool ShowStartCaptureButton => !RenderDocIsCapturing && RenderDoc.IsAvailable;
public bool ShowEndCaptureButton => RenderDocIsCapturing && RenderDoc.IsAvailable;
public static bool RenderDocIsAvailable => RenderDoc.IsAvailable;
public bool RenderDocIsCapturing
{
get;
set
{
field = value;
OnPropertyChanged();
OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton));
}
}
public static RelayCommand<MainWindowViewModel> StartRenderDocCapture { get; } =
Commands.CreateConditional<MainWindowViewModel>(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress,
viewModel =>
{
if (!RenderDoc.IsFrameCapturing)
{
if (viewModel.AppHost.RendererHost
.EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device))
{
Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture.");
}
}
viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
});
public static RelayCommand<MainWindowViewModel> EndRenderDocCapture { get; } =
Commands.CreateConditional<MainWindowViewModel>(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress,
viewModel =>
{
if (RenderDoc.IsFrameCapturing)
{
if (viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture())
{
Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture.");
}
}
viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
});
public static RelayCommand<MainWindowViewModel> DiscardRenderDocCapture { get; } =
Commands.CreateConditional<MainWindowViewModel>(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress,
viewModel =>
{
if (RenderDoc.IsFrameCapturing)
{
if (viewModel.AppHost.RendererHost.EmbeddedWindow.DiscardRenderDocCapture())
{
Logger.Info?.Print(LogClass.Application, "Discarded RenderDoc capture.");
}
}
viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
});
#endregion
}
}

View File

@@ -1,46 +1,36 @@
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.Ncm;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using DynamicData;
using Ryujinx.Ava.UI.Models;
using Ryujinx.HLE.FileSystem;
using SkiaSharp;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using Color = Avalonia.Media.Color;
using Image = SkiaSharp.SKImage;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class UserFirmwareAvatarSelectorViewModel : BaseModel
{
private static readonly Dictionary<string, byte[]> _avatarStore = new();
private static FirmwareAvatarCache _avatarCache;
[ObservableProperty]
public partial ObservableCollection<ProfileImageModel> Images { get; set; }
[ObservableProperty]
public partial Color BackgroundColor { get; set; } = Colors.White;
public Color BackgroundColor
{
get;
set
{
field = value;
OnPropertyChanged();
ChangeImageBackground();
}
} = Colors.White;
public UserFirmwareAvatarSelectorViewModel()
{
Images = [];
LoadImagesFromStore();
PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(BackgroundColor))
ChangeImageBackground();
};
}
public int SelectedIndex
@@ -65,10 +55,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
Images.Clear();
foreach (KeyValuePair<string, byte[]> image in _avatarStore)
{
Images.Add(new ProfileImageModel(image.Key, image.Value));
}
Images.AddRange(_avatarCache.CreateProfileImageModels());
}
private void ChangeImageBackground()
@@ -81,127 +68,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
{
if (_avatarStore.Count > 0)
{
return;
}
string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data);
string avatarPath = VirtualFileSystem.SwitchPathToSystemPath(contentPath);
if (!string.IsNullOrWhiteSpace(avatarPath))
{
using IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open);
Nca nca = new(virtualFileSystem.KeySet, ncaFileStream);
IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
foreach (DirectoryEntryEx item in romfs.EnumerateEntries())
{
// TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs"))
{
using UniqueRef<IFile> file = new();
romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
using MemoryStream stream = new();
using MemoryStream streamPng = new();
file.Get.AsStream().CopyTo(stream);
stream.Position = 0;
Image avatarImage = Image.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream));
using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100))
{
data.SaveTo(streamPng);
}
_avatarStore.Add(item.FullPath, streamPng.ToArray());
}
}
}
}
private static byte[] DecompressYaz0(MemoryStream stream)
{
using BinaryReader reader = new(stream);
reader.ReadInt32(); // Magic
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
reader.ReadInt64(); // Padding
byte[] input = new byte[stream.Length - stream.Position];
stream.ReadExactly(input, 0, input.Length);
uint inputOffset = 0;
byte[] output = new byte[decodedLength];
uint outputOffset = 0;
ushort mask = 0;
byte header = 0;
while (outputOffset < decodedLength)
{
if ((mask >>= 1) == 0)
{
header = input[inputOffset++];
mask = 0x80;
}
if ((header & mask) != 0)
{
if (outputOffset == output.Length)
{
break;
}
output[outputOffset++] = input[inputOffset++];
}
else
{
byte byte1 = input[inputOffset++];
byte byte2 = input[inputOffset++];
uint dist = (uint)((byte1 & 0xF) << 8) | byte2;
uint position = outputOffset - (dist + 1);
uint length = (uint)byte1 >> 4;
if (length == 0)
{
length = (uint)input[inputOffset++] + 0x12;
}
else
{
length += 2;
}
uint gap = outputOffset - position;
uint nonOverlappingLength = length;
if (nonOverlappingLength > gap)
{
nonOverlappingLength = gap;
}
Buffer.BlockCopy(output, (int)position, output, (int)outputOffset, (int)nonOverlappingLength);
outputOffset += nonOverlappingLength;
position += nonOverlappingLength;
length -= nonOverlappingLength;
while (length-- > 0)
{
output[outputOffset++] = output[position++];
}
}
}
return output;
_avatarCache ??= new FirmwareAvatarCache(contentManager, virtualFileSystem);
}
}
}

View File

@@ -61,7 +61,7 @@ namespace Ryujinx.Ava.UI.Views.Dialog
await clipboard.SetTextAsync(appData.IdString);
NotificationHelper.ShowInformation(
RyujinxNotificationManager.ShowInformation(
"Copied Title ID",
$"{appData.Name} ({appData.IdString})");
}

View File

@@ -8,7 +8,7 @@
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:common="clr-namespace:Ryujinx.Common;assembly=Ryujinx.Common"
xmlns:renderDocApi="clr-namespace:Ryujinx.Graphics.RenderDocApi;assembly=Ryujinx.Graphics.RenderDocApi"
xmlns:setupWizard="clr-namespace:Ryujinx.Ava.UI.SetupWizard"
x:DataType="viewModels:MainWindowViewModel"
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView">
<Design.DataContext>
@@ -201,29 +201,6 @@
Header="{ext:Locale GameListContextMenuManageCheat}"
Icon="{ext:Icon fa-solid fa-code}"
IsEnabled="{Binding IsGameRunning}" />
<Separator IsVisible="{Binding RenderDocIsAvailable}" />
<MenuItem
IsVisible="{Binding ShowStartCaptureButton}"
Command="{Binding StartRenderDocCapture}"
CommandParameter="{Binding}"
Header="{ext:Locale RenderDoc_MenuBarActions_StartCapture}"
Icon="{ext:Icon fa-solid fa-video}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
IsVisible="{Binding ShowEndCaptureButton}"
Command="{Binding EndRenderDocCapture}"
CommandParameter="{Binding}"
Header="{ext:Locale RenderDoc_MenuBarActions_EndCapture}"
Icon="{ext:Icon fa-solid fa-video-slash}"
IsEnabled="{Binding IsGameRunning}" />
<MenuItem
IsVisible="{Binding ShowEndCaptureButton}"
Command="{Binding DiscardRenderDocCapture}"
CommandParameter="{Binding}"
Header="{ext:Locale RenderDoc_MenuBarActions_DiscardCapture}"
ToolTip.Tip="{ext:Locale RenderDoc_MenuBarActions_DiscardCapture_ToolTip}"
Icon="{ext:Icon fa-solid fa-video-slash}"
IsEnabled="{Binding IsGameRunning}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarActions}" IsVisible="{Binding EnableNonGameRunningControls}">
<MenuItem Header="{ext:Locale MenuBarActionsInstallKeys}" Icon="{ext:Icon fa-solid fa-key}">
@@ -268,6 +245,11 @@
Header="{ext:Locale LdnGameListOpen}"
Icon="{ext:Icon fa-solid fa-people-group}"
IsEnabled="{Binding IsRyuLdnEnabled}"/>
<MenuItem
Name="SetupWizardMenuItem"
Header="{ext:Locale SetupWizardOpen}"
Icon="{ext:Icon fa-solid fa-wand-sparkles}"
IsEnabled="{x:Static setupWizard:RyujinxSetupWizardWindow.CanShowSetupWizard}"/>
<Separator />
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelpFaqAndGuides}" Icon="{ext:Icon fa-solid fa-question}" >
<MenuItem

View File

@@ -1,5 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Threading;
using Gommon;
@@ -10,6 +11,7 @@ using Ryujinx.Ava.Systems.AppLibrary;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.SetupWizard;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Views.Dialog;
using Ryujinx.Ava.UI.Windows;
@@ -30,6 +32,7 @@ namespace Ryujinx.Ava.UI.Views.Main
{
public MainWindow Window { get; private set; }
public MainMenuBarView()
{
InitializeComponent();
@@ -50,6 +53,9 @@ namespace Ryujinx.Ava.UI.Views.Main
AboutWindowMenuItem.Command = Commands.Create(AboutView.Show);
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityListWindow.Show());
LdnGameListMenuItem.Command = Commands.Create(() => LdnGamesListWindow.Show());
SetupWizardMenuItem.Command = Commands.Create(() =>
RyujinxSetupWizardWindow.ShowAsync(overwriteMode: !PollShiftPressed())
);
UpdateMenuItem.Command = MainWindowViewModel.UpdateCommand;
@@ -62,9 +68,42 @@ namespace Ryujinx.Ava.UI.Views.Main
WindowSize1440PMenuItem.Command =
WindowSize2160PMenuItem.Command = Commands.Create<string>(ChangeWindowSize);
KeyDown += OnKeyDown;
KeyUp += OnKeyUp;
LocaleManager.Instance.LocaleChanged += OnLocaleChanged;
}
/// <summary>
/// KeyUp is not reliably invoked (or invoked at all, seemingly) when a window showing up causes the main menu bar to view,
/// as shift is technically raised when that control is no longer the foreground control.
///
/// This stores <see cref="IsShiftPressed"/> to a temp variable, sets <see cref="IsShiftPressed"/> to false (if it is true), then returns the temp variable.
/// </summary>
private bool PollShiftPressed()
{
bool temp = IsShiftPressed;
if (temp)
IsShiftPressed = false;
return temp;
}
private bool IsShiftPressed { get; set; }
private void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.Key is Key.LeftShift or Key.RightShift && !IsShiftPressed)
//down is called even for keys that have been held for a while, aka key repeats.
//the check for shift being pressed prevents setting the variable every time the down event is received, if shift is already known to be pressed.
IsShiftPressed = true;
}
private void OnKeyUp(object sender, KeyEventArgs e)
{
if (e.Key is Key.LeftShift or Key.RightShift)
IsShiftPressed = false;
}
private void OnLocaleChanged()
{
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
@@ -85,24 +124,37 @@ namespace Ryujinx.Ava.UI.Views.Main
private static IEnumerable<MenuItem> GenerateLanguageMenuItems()
{
const string LanguagesPath = "Ryujinx/Assets/Languages.json";
const string LocalePath = "Ryujinx/Assets/Locale.json";
string languageJson = EmbeddedResources.ReadAllText(LanguagesPath);
string languageJson = EmbeddedResources.ReadAllText(LocalePath);
string currentLanguageCode = LocaleManager.Instance.CurrentLanguageCode;
LanguagesJson languages = JsonHelper.Deserialize(languageJson, LanguagesJsonContext.Default.LanguagesJson);
LocalesJson locales = JsonHelper.Deserialize(languageJson, LocalesJsonContext.Default.LocalesJson);
foreach ((string code, string language) in languages.Languages)
foreach (string language in locales.Languages)
{
string languageName = string.IsNullOrEmpty(language) ? code : language;
int index = locales.Locales.FindIndex(x => x.ID == "Language");
string languageName;
if (index == -1)
{
languageName = language;
}
else
{
string tr = locales.Locales[index].Translations[language];
languageName = string.IsNullOrEmpty(tr)
? language
: tr;
}
MenuItem menuItem = new()
{
Padding = new Thickness(15, 0, 0, 0),
Margin = new Thickness(3, 0, 3, 0),
HorizontalAlignment = HorizontalAlignment.Stretch,
Header = code == currentLanguageCode ? $"{languageName} ✔" : languageName,
Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(code))
Header = language == currentLanguageCode ? $"{languageName} ✔" : languageName,
Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(language))
};
yield return menuItem;
@@ -132,11 +184,13 @@ namespace Ryujinx.Ava.UI.Views.Main
}
else
{
bool customConfigExists = File.Exists(Program.GetDirGameUserConfig(ViewModel.SelectedApplication.IdString));
bool customConfigExists =
File.Exists(Program.GetDirGameUserConfig(ViewModel.SelectedApplication.IdString));
if (!ViewModel.IsGameRunning || !customConfigExists)
{
await Window.SettingsWindow.ShowDialog(Window); // The game is not running, or if the user configuration does not exist
await Window.SettingsWindow
.ShowDialog(Window); // The game is not running, or if the user configuration does not exist
}
else
{
@@ -160,7 +214,8 @@ namespace Ryujinx.Ava.UI.Views.Main
if (!MiiApplet.CanStart(out ApplicationData appData, out BlitStruct<ApplicationControlProperty> nacpData))
return;
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData);
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen,
nacpData);
}
public async Task OpenCheatManagerForCurrentApp()
@@ -168,7 +223,8 @@ namespace Ryujinx.Ava.UI.Views.Main
if (!ViewModel.IsGameRunning)
return;
string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString();
string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties
.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString();
await StyleableAppWindow.ShowAsync(
new CheatWindow(
@@ -197,18 +253,24 @@ namespace Ryujinx.Ava.UI.Views.Main
{
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();
if (ViewModel.AreMimeTypesRegistered)
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
await ContentDialogHelper.CreateInfoDialog(
LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesSuccessMessage], string.Empty,
LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
else
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]);
await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]);
}
private async Task UninstallFileTypes()
{
ViewModel.AreMimeTypesRegistered = !FileAssociationHelper.Uninstall();
if (!ViewModel.AreMimeTypesRegistered)
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], string.Empty, LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
await ContentDialogHelper.CreateInfoDialog(
LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesSuccessMessage], string.Empty,
LocaleManager.Instance[LocaleKeys.InputDialogOk], string.Empty, string.Empty);
else
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesErrorMessage]);
await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesErrorMessage]);
}
private void ChangeWindowSize(string resolution)
@@ -220,7 +282,7 @@ namespace Ryujinx.Ava.UI.Views.Main
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
double barsHeight = ((Window.StatusBarHeight + Window.MenuBarHeight) +
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor);
double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor);

View File

@@ -61,7 +61,7 @@ namespace Ryujinx.Ava.UI.Views.Misc
await clipboard.SetTextAsync(appData.IdString);
NotificationHelper.ShowInformation(
RyujinxNotificationManager.ShowInformation(
"Copied Title ID",
$"{appData.Name} ({appData.IdString})");
}

View File

@@ -18,7 +18,6 @@
<viewModels:CompatibilityViewModel />
</window:StyleableAppWindow.DataContext>
<Grid RowDefinitions="Auto,Auto,*">
<!-- UI FlushControls -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto,Auto" Name="FlushControls">
<controls:RyujinxLogo

View File

@@ -41,8 +41,6 @@
<KeyBinding Gesture="Escape" Command="{Binding ExitCurrentState}" />
<KeyBinding Gesture="Ctrl+A" Command="{Binding OpenAmiiboWindow}" />
<KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" />
<KeyBinding Gesture="Ctrl+Shift+R" Command="{Binding ReloadRenderDocApi}" />
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding ToggleCapture}" />
</Window.KeyBindings>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*">
<helpers:OffscreenTextBox IsEnabled="False" Opacity="0" Name="HiddenTextBox" IsHitTestVisible="False" IsTabStop="False" />

View File

@@ -30,6 +30,7 @@ using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL3;
using Ryujinx.Ava.UI.SetupWizard;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -134,11 +135,18 @@ namespace Ryujinx.Ava.UI.Windows
{
base.OnApplyTemplate(e);
NotificationHelper.SetNotificationManager(this);
RyujinxNotificationManager.Shared = new RyujinxNotificationManager(this);
Executor.ExecuteBackgroundAsync(async () =>
{
await ShowIntelMacWarningAsync();
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await ShowIntelMacWarningAsync();
if (Program.IsFirstStart)
await RyujinxSetupWizardWindow.ShowAsync(overwriteMode: false, this);
});
if (CommandLineState.FirmwareToInstallPathArg.TryGet(out FilePath fwPath))
{
if (fwPath is { ExistsAsFile: true, Extension: "xci" or "zip" } || fwPath.ExistsAsDirectory)
@@ -150,6 +158,8 @@ namespace Ryujinx.Ava.UI.Windows
else
Logger.Notice.Print(LogClass.UI, "Invalid firmware type provided. Path must be a directory, or a .zip or .xci file.");
}
await CheckLaunchState();
});
}
@@ -399,7 +409,7 @@ namespace Ryujinx.Ava.UI.Windows
}
}
}
else
else if (!RyujinxSetupWizardWindow.IsOpen)
{
ShowKeyErrorOnLoad = false;
@@ -538,8 +548,6 @@ namespace Ryujinx.Ava.UI.Windows
{
LoadApplications();
}
_ = CheckLaunchState();
}
private void SetMainContent(Control content = null)

View File

@@ -19,9 +19,6 @@ namespace Ryujinx.Ava.Utilities
public static string OverrideSystemLanguage { get; private set; }
public static string OverrideHideCursor { get; private set; }
public static string BaseDirPathArg { get; private set; }
public static string RenderDocCaptureTitleFormat { get; private set; } =
"{EmuVersion}\n{GuestName} {GuestVersion} {GuestTitleId} {GuestArch}";
public static Optional<FilePath> FirmwareToInstallPathArg { get; set; }
public static string Profile { get; private set; }
public static string LaunchPathArg { get; private set; }
@@ -57,20 +54,6 @@ namespace Ryujinx.Ava.Utilities
BaseDirPathArg = args[++i];
arguments.Add(arg);
arguments.Add(args[i]);
break;
case "-rdct":
case "--rd-capture-title-format":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
RenderDocCaptureTitleFormat = args[++i];
arguments.Add(arg);
arguments.Add(args[i]);
break;

View File

@@ -1,4 +1,3 @@
using Gommon;
using Ryujinx.HLE.Loaders.Processes;
namespace Ryujinx.Ava.Utilities
@@ -23,23 +22,5 @@ namespace Ryujinx.Ava.Utilities
? appTitle + $" ({pauseString})"
: appTitle;
}
public static string FormatRenderDocCaptureTitle(ProcessResult activeProcess, string applicationVersion)
{
if (activeProcess == null)
return string.Empty;
string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : activeProcess.Name;
string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $"v{activeProcess.DisplayVersion}";
string titleIdSection = $"({activeProcess.ProgramIdText.ToUpper()})";
string titleArchSection = activeProcess.Is64Bit ? "(64-bit)" : "(32-bit)";
return CommandLineState.RenderDocCaptureTitleFormat
.ReplaceIgnoreCase("{EmuVersion}", applicationVersion)
.ReplaceIgnoreCase("{GuestName}", titleNameSection)
.ReplaceIgnoreCase("{GuestVersion}", titleVersionSection)
.ReplaceIgnoreCase("{GuestTitleId}", titleIdSection)
.ReplaceIgnoreCase("{GuestArch}", titleArchSection);
}
}
}