mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-02-20 15:51:09 +00:00
Compare commits
41 Commits
Canary-1.3
...
setup-wiza
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7dd718d6f | ||
|
|
6ee7957574 | ||
|
|
bf62531802 | ||
|
|
17be50ea80 | ||
|
|
ec50a1ec3e | ||
|
|
5a20047e5e | ||
|
|
f9fed4cf4d | ||
|
|
2970dcd3c7 | ||
|
|
4be6cb2fa1 | ||
|
|
c90d2af9cd | ||
|
|
13ff9cb162 | ||
|
|
b35ba58831 | ||
|
|
e12a77d4a3 | ||
|
|
804a4e0bcb | ||
|
|
94870eafaa | ||
|
|
7e6cc31866 | ||
|
|
3b25c43abf | ||
|
|
1804dd031b | ||
|
|
211498e060 | ||
|
|
4bdee89288 | ||
|
|
d8a6364cca | ||
|
|
2f794794c6 | ||
|
|
1d6c2426df | ||
|
|
6cd03f15fa | ||
|
|
3fe7600382 | ||
|
|
dc2aa837b3 | ||
|
|
133ac41425 | ||
|
|
fd2ecee479 | ||
|
|
8f529d17a8 | ||
|
|
884d0f526c | ||
|
|
c5b325bde2 | ||
|
|
8ab851ead8 | ||
|
|
5a060cf451 | ||
|
|
9b0fa3bf6d | ||
|
|
325e13a490 | ||
|
|
e202cccc6e | ||
|
|
e0ed8f56ea | ||
|
|
46b2fb92d7 | ||
|
|
8563e7d4dc | ||
|
|
ee10cbf735 | ||
|
|
b033adbde7 |
12
.github/workflows/canary.yml
vendored
12
.github/workflows/canary.yml
vendored
@@ -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: |
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": "繁體中文 (台灣)"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,11 @@ namespace Ryujinx.Common
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_queue.CompleteAdding();
|
||||
try
|
||||
{
|
||||
_queue.CompleteAdding();
|
||||
} catch (ObjectDisposedException) {}
|
||||
|
||||
_cts.Cancel();
|
||||
_workerThread.Join();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
37
src/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs
Normal file
37
src/Ryujinx.Common/Utilities/TypedStringEnumConverter.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
|
||||
namespace Ryujinx.HLE.Exceptions
|
||||
{
|
||||
class InvalidFirmwarePackageException : Exception
|
||||
public class InvalidFirmwarePackageException : Exception
|
||||
{
|
||||
public InvalidFirmwarePackageException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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]}");
|
||||
}
|
||||
|
||||
@@ -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 (LocalesJson file in _localeData.Value.LocalesFiles.Values)
|
||||
{
|
||||
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>(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;
|
||||
}
|
||||
|
||||
32
src/Ryujinx/Common/UIImages.cs
Normal file
32
src/Ryujinx/Common/UIImages.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -32,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; }
|
||||
@@ -187,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)
|
||||
@@ -221,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);
|
||||
|
||||
@@ -247,6 +245,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
ConfigurationState.Instance.LoadDefault();
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
|
||||
IsFirstStart = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -262,6 +261,8 @@ namespace Ryujinx.Ava
|
||||
|
||||
ConfigurationFileFormat.RenameInvalidConfigFile(ConfigurationPath);
|
||||
|
||||
IsFirstStart = true;
|
||||
|
||||
ConfigurationState.Instance.LoadDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,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" />
|
||||
@@ -156,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" />
|
||||
@@ -178,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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
179
src/Ryujinx/UI/Helpers/RyujinxNotificationManager.cs
Normal file
179
src/Ryujinx/UI/Helpers/RyujinxNotificationManager.cs
Normal 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
|
||||
}
|
||||
}
|
||||
172
src/Ryujinx/UI/Models/FirmwareAvatarCache.cs
Normal file
172
src/Ryujinx/UI/Models/FirmwareAvatarCache.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Ava.UI.SetupWizard;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -56,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
27
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePage.axaml
Normal file
27
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePage.axaml
Normal 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>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
|
||||
namespace Ryujinx.Ava.UI.SetupWizard.Pages
|
||||
{
|
||||
public partial class SetupFirmwarePage : RyujinxControl<SetupFirmwarePageContext>
|
||||
{
|
||||
public SetupFirmwarePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
167
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs
Normal file
167
src/Ryujinx/UI/SetupWizard/Pages/Fw/SetupFirmwarePageContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml
Normal file
22
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml
Normal 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>
|
||||
|
||||
13
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml.cs
Normal file
13
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPage.axaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
130
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs
Normal file
130
src/Ryujinx/UI/SetupWizard/Pages/Keys/SetupKeysPageContext.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
3
src/Ryujinx/UI/SetupWizard/README.md
Normal file
3
src/Ryujinx/UI/SetupWizard/README.md
Normal 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).
|
||||
82
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs
Normal file
82
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.Steps.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
144
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs
Normal file
144
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizard.cs
Normal 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
|
||||
}
|
||||
}
|
||||
18
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml
Normal file
18
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml
Normal 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>
|
||||
90
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs
Normal file
90
src/Ryujinx/UI/SetupWizard/RyujinxSetupWizardWindow.axaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs
Normal file
94
src/Ryujinx/UI/SetupWizard/SetupWizardPage.Builder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs
Normal file
67
src/Ryujinx/UI/SetupWizard/SetupWizardPage.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs
Normal file
35
src/Ryujinx/UI/SetupWizard/SetupWizardPageContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
92
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml
Normal file
92
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml
Normal 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>
|
||||
|
||||
23
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml.cs
Normal file
23
src/Ryujinx/UI/SetupWizard/SetupWizardPageView.axaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -45,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;
|
||||
@@ -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)
|
||||
{
|
||||
@@ -1947,14 +1950,14 @@ 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(
|
||||
RyujinxNotificationManager.ShowInformation(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, ConfigurationState.Instance.Debug.GdbStubPort.Value),
|
||||
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckGdbStubMessage]);
|
||||
}
|
||||
@@ -1973,7 +1976,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
_ => LocaleKeys.SettingsTabSystemDramSize4GiB,
|
||||
};
|
||||
|
||||
NotificationHelper.ShowWarning(
|
||||
RyujinxNotificationManager.ShowWarning(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.NotificationLaunchCheckDramSizeTitle,
|
||||
LocaleManager.Instance[memoryConfigurationLocaleKey]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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})");
|
||||
}
|
||||
|
||||
@@ -8,6 +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:setupWizard="clr-namespace:Ryujinx.Ava.UI.SetupWizard"
|
||||
x:DataType="viewModels:MainWindowViewModel"
|
||||
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView">
|
||||
<Design.DataContext>
|
||||
@@ -244,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
|
||||
|
||||
@@ -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,16 +124,29 @@ 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()
|
||||
{
|
||||
@@ -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);
|
||||
|
||||
@@ -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})");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user