Compare commits

..

3 Commits

55 changed files with 340 additions and 2844 deletions

View File

@@ -201,8 +201,8 @@ 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 }} -p publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
./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 1
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -p publish/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_arm64.app.tar.gz
create_gitlab_release:
name: Create GitLab Release

View File

@@ -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 }} -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_arm64.app.tar.gz
create_gitlab_release:
name: Create GitLab Release

View File

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

View File

@@ -24841,781 +24841,6 @@
"zh_CN": "如果您在设置中有某些 LDN 口令则可加入此游戏。",
"zh_TW": "你只能加入與 LDN 網路密碼片語 (passphrase) 設定相同的遊戲。"
}
},
{
"ID": "SetupWizardOpen",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Setup Wizard",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardActionBack",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Back",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardActionNext",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Next",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirstPageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Welcome to Ryubing!",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirstPageContent",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Ryubing is a fork of the discontinued Nintendo Switch emulator, Ryujinx.\n\nThis setup wizard will guide you through the necessary steps needed for Ryubing to play your Switch games on PC.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirstPageAction",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Start Setup",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardKeysPageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Key Files",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardKeysPageDescription",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder containing your prod/title .keys files:",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardKeysPageFolderPopupTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder containing your prod/title .keys files",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardKeysPageHelpText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Not sure how to get your keys?",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardKeysPageSkipText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Skipped setting up keys as you already have a valid key installation and did not choose a folder to install from.\nClick '{0}' if you wish to reinstall your keys.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Switch Firmware",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageDescription",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder or .zip/.xci containing your dumped Nintendo Switch firmware:",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageFolderPopupTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the folder containing your dumped & extracted Switch firmware.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageFilePopupTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Please select the file containing your dumped Switch firmware.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageFileBrowse",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Select .zip or .xci",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageFolderBrowse",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Select Extracted Folder",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageInstallSuccessNotificationTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Firmware installed",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageInstallSuccessNotificationText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Installed firmware version {0}.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageInstallFailNotificationTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Firmware not installed",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageInstallFailNotificationText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "It seems some error occurred when trying to install the firmware at path '{0}'.\nDid that folder contain a firmware dump?",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageHelpText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Not sure how to get your firmware off of your Switch?",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFirmwarePageSkipText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Skipped setting up firmware as you already have a valid firmware installation and did not choose a folder or file to install from.\nClick '{0}' if you wish to overwrite your firmware.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardGameDirsPageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Game, Update, and DLC Paths",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardGameDirsPageDescription",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "{0} can be pointed at any number of folders to look for your games, updates, and DLC content.\nAt least one folder must be specified in game directories before continuing.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardGameDirsPageNoFoldersSelectedError",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "At least one folder for games must be selected; otherwise the UI will be empty.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardGameDirsPageHelpText",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Not sure how to get your games, updates, and/or DLC onto your PC?",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFinalPageTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Setup Complete",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFinalPageDescription",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Your installation of Ryubing (aka Ryujinx) has been completed.\n\nIf you require assistance, feel free to join our Discord server and ask for help,\nafter verifying your possession of a modded Nintendo Switch.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardFinalPageAction",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Finish",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SetupWizardHelpLinkButton",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Click here to view a guide.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
}
]
}
}

View File

@@ -1,95 +0,0 @@
import argparse
import os
from pathlib import Path
import platform
import shutil
import subprocess
parser = argparse.ArgumentParser(
description="Construct Universal dylibs for nuget package"
)
parser.add_argument(
"arm64_input_directory", help="ARM64 Input directory containing dylibs"
)
parser.add_argument(
"x86_64_input_directory", help="x86_64 Input directory containing dylibs"
)
parser.add_argument("output_directory", help="Output directory")
parser.add_argument("rglob", help="rglob")
args = parser.parse_args()
# Use Apple LLVM on Darwin, otherwise standard LLVM.
if platform.system() == "Darwin":
LIPO = "lipo"
else:
LIPO = shutil.which("llvm-lipo")
if LIPO is None:
for llvm_ver in [17, 16, 15, 14, 13]:
lipo_path = shutil.which(f"llvm-lipo-{llvm_ver}")
if lipo_path is not None:
LIPO = lipo_path
break
if LIPO is None:
raise Exception("Cannot find a valid location for LLVM lipo!")
arm64_input_directory: Path = Path(args.arm64_input_directory)
x86_64_input_directory: Path = Path(args.x86_64_input_directory)
output_directory: Path = Path(args.output_directory)
rglob = args.rglob
def get_new_name(
input_directory: Path, output_directory: str, input_dylib_path: Path
) -> Path:
input_component = str(input_dylib_path).replace(str(input_directory), "")[1:]
return Path(os.path.join(output_directory, input_component))
def is_fat_file(dylib_path: Path) -> str:
res = subprocess.check_output([LIPO, "-info", str(dylib_path.absolute())]).decode(
"utf-8"
)
return not res.split("\n")[0].startswith("Non-fat file")
def construct_universal_dylib(
arm64_input_dylib_path: Path, x86_64_input_dylib_path: Path, output_dylib_path: Path
):
if output_dylib_path.exists() or output_dylib_path.is_symlink():
os.remove(output_dylib_path)
os.makedirs(output_dylib_path.parent, exist_ok=True)
if arm64_input_dylib_path.is_symlink():
os.symlink(
os.path.basename(arm64_input_dylib_path.resolve()), output_dylib_path
)
else:
if is_fat_file(arm64_input_dylib_path) or not x86_64_input_dylib_path.exists():
with open(output_dylib_path, "wb") as dst:
with open(arm64_input_dylib_path, "rb") as src:
dst.write(src.read())
else:
subprocess.check_call(
[
LIPO,
str(arm64_input_dylib_path.absolute()),
str(x86_64_input_dylib_path.absolute()),
"-output",
str(output_dylib_path.absolute()),
"-create",
]
)
print(rglob)
for path in arm64_input_directory.rglob("**/*.dylib"):
construct_universal_dylib(
path,
get_new_name(arm64_input_directory, x86_64_input_directory, path),
get_new_name(arm64_input_directory, output_directory, path),
)

View File

@@ -14,7 +14,7 @@ mkdir "$APP_BUNDLE_DIRECTORY/Contents/Frameworks"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/MacOS"
mkdir "$APP_BUNDLE_DIRECTORY/Contents/Resources"
# Copy executable and nsure executable can be executed
# Copy executable and ensure executable can be executed
cp "$PUBLISH_DIRECTORY/Ryujinx" "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"
chmod u+x "$APP_BUNDLE_DIRECTORY/Contents/MacOS/Ryujinx"

View File

@@ -33,69 +33,44 @@ if [[ "$(uname)" == "Darwin" ]]; then
fi
if [ "$CANARY" == "1" ]; then
RELEASE_TAR_FILE_NAME=ryujinx-canary-$VERSION-macos_universal.app.tar
RELEASE_TAR_FILE_NAME=ryujinx-canary-$VERSION-macos_arm64.app.tar
elif [ "$VERSION" == "1.1.0" ]; then
RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_arm64.app.tar
else
RELEASE_TAR_FILE_NAME=ryujinx-$VERSION-macos_universal.app.tar
RELEASE_TAR_FILE_NAME=ryujinx-$VERSION-macos_arm64.app.tar
fi
ARM64_APP_BUNDLE="$TEMP_DIRECTORY/output_arm64/Ryujinx.app"
X64_APP_BUNDLE="$TEMP_DIRECTORY/output_x64/Ryujinx.app"
UNIVERSAL_APP_BUNDLE="$OUTPUT_DIRECTORY/Ryujinx.app"
OUTPUT_APP_BUNDLE="$OUTPUT_DIRECTORY/Ryujinx.app"
EXECUTABLE_SUB_PATH=Contents/MacOS/Ryujinx
rm -rf "$TEMP_DIRECTORY"
mkdir -p "$TEMP_DIRECTORY"
DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" --self-contained true $EXTRA_ARGS)
DOTNET_COMMON_ARGS=(-p:DebugType=embedded -p:Version="$VERSION" -p:SourceRevisionId="$SOURCE_REVISION_ID" --self-contained $EXTRA_ARGS)
dotnet restore
dotnet build -c "$CONFIGURATION" src/Ryujinx
dotnet publish -c "$CONFIGURATION" -r osx-arm64 -o "$TEMP_DIRECTORY/publish_arm64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
dotnet publish -c "$CONFIGURATION" -r osx-x64 -o "$TEMP_DIRECTORY/publish_x64" "${DOTNET_COMMON_ARGS[@]}" src/Ryujinx
# Get rid of the support library for ARMeilleure for x64 (that's only for arm64)
rm -rf "$TEMP_DIRECTORY/publish_x64/libarmeilleure-jitsupport.dylib"
# Get rid of libsoundio from arm64 builds as we don't have a arm64 variant
# TODO: remove this once done
rm -rf "$TEMP_DIRECTORY/publish_arm64/libsoundio.dylib"
pushd "$BASE_DIR/distribution/macos"
./create_app_bundle.sh "$TEMP_DIRECTORY/publish_x64" "$TEMP_DIRECTORY/output_x64" "$ENTITLEMENTS_FILE_PATH"
./create_app_bundle.sh "$TEMP_DIRECTORY/publish_arm64" "$TEMP_DIRECTORY/output_arm64" "$ENTITLEMENTS_FILE_PATH"
popd
rm -rf "$UNIVERSAL_APP_BUNDLE"
rm -rf "$OUTPUT_APP_BUNDLE"
mkdir -p "$OUTPUT_DIRECTORY"
# Let's copy one of the two different app bundle and remove the executable
cp -R "$ARM64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE"
rm "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH"
# Make its libraries universal
python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_BUNDLE" "$X64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE" "**/*.dylib"
if ! [ -x "$(command -v lipo)" ];
then
if ! [ -x "$(command -v llvm-lipo-17)" ];
then
LIPO=llvm-lipo
else
LIPO=llvm-lipo-17
fi
else
LIPO=lipo
fi
# Make the executable universal
$LIPO "$ARM64_APP_BUNDLE/$EXECUTABLE_SUB_PATH" "$X64_APP_BUNDLE/$EXECUTABLE_SUB_PATH" -output "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH" -create
# Let's copy the app bundle to the output folder so we can package it freely
cp -R "$ARM64_APP_BUNDLE" "$OUTPUT_APP_BUNDLE"
# Patch up the Info.plist to have appropriate version
sed -r -i.bck "s/\%\%RYUJINX_BUILD_VERSION\%\%/$VERSION/g;" "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist"
sed -r -i.bck "s/\%\%RYUJINX_BUILD_GIT_HASH\%\%/$SOURCE_REVISION_ID/g;" "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist"
rm "$UNIVERSAL_APP_BUNDLE/Contents/Info.plist.bck"
sed -r -i.bck "s/\%\%RYUJINX_BUILD_VERSION\%\%/$VERSION/g;" "$OUTPUT_APP_BUNDLE/Contents/Info.plist"
sed -r -i.bck "s/\%\%RYUJINX_BUILD_GIT_HASH\%\%/$SOURCE_REVISION_ID/g;" "$OUTPUT_APP_BUNDLE/Contents/Info.plist"
rm "$OUTPUT_APP_BUNDLE/Contents/Info.plist.bck"
# Now sign it
if ! [ -x "$(command -v codesign)" ];
@@ -109,10 +84,10 @@ then
# NOTE: Currently require https://github.com/indygreg/apple-platform-rs/pull/44 to work on other OSes.
# cargo install --git "https://github.com/marysaka/apple-platform-rs" --branch "fix/adhoc-app-bundle" apple-codesign --bin "rcodesign"
echo "Using rcodesign for ad-hoc signing"
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$UNIVERSAL_APP_BUNDLE"
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$OUTPUT_APP_BUNDLE"
else
echo "Using codesign for ad-hoc signing"
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$UNIVERSAL_APP_BUNDLE"
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$OUTPUT_APP_BUNDLE"
fi
echo "Creating archive"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,6 @@ 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] }
});

View File

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

View File

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

View File

@@ -32,8 +32,6 @@ 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; }
@@ -189,9 +187,12 @@ namespace Ryujinx.Ava
DriverUtilities.InitDriverConfig(ConfigurationState.Instance.Graphics.BackendThreading == BackendThreading.Off);
// Check if keys exists.
if (!File.Exists(Path.Combine(AppDataManager.GetKeysDir(), "prod.keys")))
if (!File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")))
{
MainWindow.ShowKeyErrorOnLoad = true;
if (!(AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"))))
{
MainWindow.ShowKeyErrorOnLoad = true;
}
}
if (CommandLineState.LaunchPathArg != null)
@@ -220,6 +221,7 @@ 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);
@@ -245,7 +247,6 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.LoadDefault();
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
IsFirstStart = true;
}
else
{
@@ -261,8 +262,6 @@ namespace Ryujinx.Ava
ConfigurationFileFormat.RenameInvalidConfigFile(ConfigurationPath);
IsFirstStart = true;
ConfigurationState.Instance.LoadDefault();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,6 @@ 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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Styling;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.Common;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using System;
@@ -37,17 +36,21 @@ 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);
DiscordLogo = UIImages.GetLogoByNameAndTheme("Discord", isDarkTheme)
.CreateScaledBitmap(new PixelSize(32, 24));
GitLabLogo = UIImages.GetLogoByNameAndTheme("GitLab", isDarkTheme)
.CreateScaledBitmap(new PixelSize(32, 31));
string themeName = isDarkTheme ? "Dark" : "Light";
DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
GitLabLogo = LoadBitmap(LogoPathFormat.Format("GitLab", themeName));
}
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
public void Dispose()
{
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;

View File

@@ -45,7 +45,6 @@ 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;
@@ -871,7 +870,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void RefreshGrid()
{
_ = Applications.ToObservableChangeSet()
IObservableList<ApplicationData> appsList = Applications.ToObservableChangeSet()
.Filter(Filter)
.Sort(GetComparer())
.Bind(out ReadOnlyObservableCollection<ApplicationData> apps)
@@ -1014,11 +1013,16 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public async Task HandleKeysInstallation(string filename)
private async Task HandleKeysInstallation(string filename)
{
try
{
string systemDirectory = AppDataManager.GetKeysDir();
string systemDirectory = AppDataManager.KeysDirPath;
if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile &&
Directory.Exists(AppDataManager.KeysDirPathUser))
{
systemDirectory = AppDataManager.KeysDirPathUser;
}
string dialogTitle =
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallTitle);
@@ -1372,8 +1376,8 @@ namespace Ryujinx.Ava.UI.ViewModels
Patterns = ["*.zip"],
AppleUniformTypeIdentifiers = ["public.zip-archive"],
MimeTypes = ["application/zip"],
}
}
},
},
});
if (result.HasValue)
@@ -1753,19 +1757,12 @@ namespace Ryujinx.Ava.UI.ViewModels
public static void UpdateGameMetadata(string titleId, TimeSpan playTime)
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime));
/// <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)
public void RefreshFirmwareStatus()
{
SystemVersion version = null;
try
{
if (!allowNullVersion)
version ??= ContentManager.GetCurrentFirmwareVersion();
version = ContentManager.GetCurrentFirmwareVersion();
}
catch (Exception)
{
@@ -1950,14 +1947,14 @@ namespace Ryujinx.Ava.UI.ViewModels
{
if (ConfigurationState.Instance.Debug.DebuggerSuspendOnStart)
{
RyujinxNotificationManager.ShowInformation(
NotificationHelper.ShowInformation(
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckSuspendOnStartTitle],
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckSuspendOnStartMessage]);
}
if (ConfigurationState.Instance.Debug.EnableGdbStub)
{
RyujinxNotificationManager.ShowInformation(
NotificationHelper.ShowInformation(
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, ConfigurationState.Instance.Debug.GdbStubPort.Value),
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckGdbStubMessage]);
}
@@ -1976,7 +1973,7 @@ namespace Ryujinx.Ava.UI.ViewModels
_ => LocaleKeys.SettingsTabSystemDramSize4GiB,
};
RyujinxNotificationManager.ShowWarning(
NotificationHelper.ShowWarning(
LocaleManager.Instance.UpdateAndGetDynamicValue(
LocaleKeys.NotificationLaunchCheckDramSizeTitle,
LocaleManager.Instance[memoryConfigurationLocaleKey]

View File

@@ -1,36 +1,46 @@
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
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.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 FirmwareAvatarCache _avatarCache;
private static readonly Dictionary<string, byte[]> _avatarStore = new();
[ObservableProperty]
public partial ObservableCollection<ProfileImageModel> Images { get; set; }
public Color BackgroundColor
{
get;
set
{
field = value;
OnPropertyChanged();
ChangeImageBackground();
}
} = Colors.White;
[ObservableProperty]
public partial Color BackgroundColor { get; set; } = Colors.White;
public UserFirmwareAvatarSelectorViewModel()
{
Images = [];
LoadImagesFromStore();
PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(BackgroundColor))
ChangeImageBackground();
};
}
public int SelectedIndex
@@ -55,7 +65,10 @@ namespace Ryujinx.Ava.UI.ViewModels
{
Images.Clear();
Images.AddRange(_avatarCache.CreateProfileImageModels());
foreach (KeyValuePair<string, byte[]> image in _avatarStore)
{
Images.Add(new ProfileImageModel(image.Key, image.Value));
}
}
private void ChangeImageBackground()
@@ -68,7 +81,127 @@ namespace Ryujinx.Ava.UI.ViewModels
public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
{
_avatarCache ??= new FirmwareAvatarCache(contentManager, 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;
}
}
}

View File

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

View File

@@ -8,7 +8,6 @@
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>
@@ -245,11 +244,6 @@
Header="{ext:Locale LdnGameListOpen}"
Icon="{ext:Icon fa-solid fa-people-group}"
IsEnabled="{Binding IsRyuLdnEnabled}"/>
<MenuItem
Name="SetupWizardMenuItem"
Header="{ext:Locale SetupWizardOpen}"
Icon="{ext:Icon fa-solid fa-wand-sparkles}"
IsEnabled="{x:Static setupWizard:RyujinxSetupWizardWindow.CanShowSetupWizard}"/>
<Separator />
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelpFaqAndGuides}" Icon="{ext:Icon fa-solid fa-question}" >
<MenuItem

View File

@@ -1,6 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Threading;
using Gommon;
@@ -11,7 +10,6 @@ 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;
@@ -32,7 +30,6 @@ namespace Ryujinx.Ava.UI.Views.Main
{
public MainWindow Window { get; private set; }
public MainMenuBarView()
{
InitializeComponent();
@@ -53,9 +50,6 @@ 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;
@@ -68,42 +62,9 @@ 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();
@@ -184,13 +145,11 @@ 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
{
@@ -214,8 +173,7 @@ 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()
@@ -223,8 +181,7 @@ 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(
@@ -253,24 +210,18 @@ 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)
@@ -282,7 +233,7 @@ namespace Ryujinx.Ava.UI.Views.Main
// Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024)
double barsHeight = ((Window.StatusBarHeight + Window.MenuBarHeight) +
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
(ConfigurationState.Instance.ShowOldUI ? (int)Window.TitleBar.Height : 0));
double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor);
double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor);

View File

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

View File

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

View File

@@ -30,7 +30,6 @@ 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;
@@ -135,18 +134,10 @@ namespace Ryujinx.Ava.UI.Windows
{
base.OnApplyTemplate(e);
RyujinxNotificationManager.Shared = new RyujinxNotificationManager(this);
NotificationHelper.SetNotificationManager(this);
Executor.ExecuteBackgroundAsync(async () =>
{
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)
@@ -158,8 +149,6 @@ 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();
});
}
@@ -409,7 +398,7 @@ namespace Ryujinx.Ava.UI.Windows
}
}
}
else if (!RyujinxSetupWizardWindow.IsOpen)
else
{
ShowKeyErrorOnLoad = false;
@@ -548,6 +537,8 @@ namespace Ryujinx.Ava.UI.Windows
{
LoadApplications();
}
_ = CheckLaunchState();
}
private void SetMainContent(Control content = null)
@@ -765,20 +756,6 @@ namespace Ryujinx.Ava.UI.Windows
});
}
private static bool _intelMacWarningShown = !RunningPlatform.IsIntelMac;
public static async Task ShowIntelMacWarningAsync()
{
if (_intelMacWarningShown)
return;
await Dispatcher.UIThread.InvokeAsync(async () => await ContentDialogHelper.CreateWarningDialog(
"Intel Mac Warning",
"Intel Macs are not supported and will not work properly.\nIf you continue, do not come to our Discord asking for support;\nand do not report bugs on the GitHub. They will be closed."));
_intelMacWarningShown = true;
}
private void AppWindow_OnGotFocus(object sender, GotFocusEventArgs e)
{
if (ViewModel.AppHost is null)