mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-06-12 07:19:14 +00:00
Compare commits
110 Commits
Canary-1.3
...
719d879720
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
719d879720 | ||
|
|
ecd503cf20 | ||
|
|
36d9466427 | ||
|
|
6282db2cb0 | ||
|
|
6ea4ce3404 | ||
|
|
72c88b9b50 | ||
|
|
0baf3b84ed | ||
|
|
1552d31e01 | ||
|
|
5d0fc6d456 | ||
|
|
5993c78344 | ||
|
|
e1b01f2e70 | ||
|
|
e3bdda7afa | ||
|
|
0d5503c014 | ||
|
|
b26375cc4b | ||
|
|
85dad79581 | ||
|
|
2941951f4d | ||
|
|
925ae1652b | ||
|
|
af59454e19 | ||
|
|
beb7dfe7a6 | ||
|
|
85e62c3ad2 | ||
|
|
082a2a2051 | ||
|
|
2b2159f330 | ||
|
|
3620c76cc1 | ||
|
|
5a8f6fa46d | ||
|
|
813057acbf | ||
|
|
620eba5fcb | ||
|
|
829a5561ab | ||
|
|
0aec32f437 | ||
|
|
6619de59ab | ||
|
|
746dc2cd22 | ||
|
|
17ab09119e | ||
|
|
46bae1c40a | ||
|
|
5536bba1fa | ||
|
|
8c6b642129 | ||
|
|
76dd747811 | ||
|
|
1c073ebc63 | ||
|
|
cebe423df2 | ||
|
|
93f53b24b8 | ||
|
|
cb5c9ce585 | ||
|
|
167b41354b | ||
|
|
e52fa0b9d1 | ||
|
|
6ed92dd9b7 | ||
|
|
d9846faa5f | ||
|
|
928a189d99 | ||
|
|
de11115971 | ||
|
|
c093b34767 | ||
|
|
13036dcd5b | ||
|
|
51bec1f4a2 | ||
|
|
00cb9e42f8 | ||
|
|
5be56d0ccf | ||
|
|
a9bb932491 | ||
|
|
e1f215de46 | ||
|
|
b6eb78598c | ||
|
|
1392fcfbc5 | ||
|
|
9b82e8452f | ||
|
|
379ce9e7aa | ||
|
|
fc89c17037 | ||
|
|
222db1a736 | ||
|
|
89c6c490a3 | ||
|
|
4d3a98e71d | ||
|
|
18233cf7e6 | ||
|
|
c0cc54cc56 | ||
|
|
d9ab68b1e9 | ||
|
|
e7e0d4d877 | ||
|
|
a2fa346cfd | ||
|
|
6ae279300c | ||
|
|
82e392604d | ||
|
|
860112c910 | ||
|
|
0ecef83316 | ||
|
|
93256afd24 | ||
|
|
5d3f22ac57 | ||
|
|
5b63aabe8b | ||
|
|
9b6dfab66e | ||
|
|
9e1ee169d9 | ||
|
|
0958796a29 | ||
|
|
f257481cdb | ||
|
|
6533270499 | ||
|
|
c3c6f36fea | ||
|
|
df153efadf | ||
|
|
4b2362f18b | ||
|
|
c2fc1a8582 | ||
|
|
598d6076ee | ||
|
|
69e2ea2894 | ||
|
|
93fe2d36aa | ||
|
|
0d2f280303 | ||
|
|
24ac55f4d6 | ||
|
|
af5d9a90b7 | ||
|
|
d52415b535 | ||
|
|
4d56f4dcd3 | ||
|
|
57c91089f7 | ||
|
|
15c9d50815 | ||
|
|
4012fecc25 | ||
|
|
80df6e2336 | ||
|
|
9e2837d885 | ||
|
|
96028daff1 | ||
|
|
46fa8c1426 | ||
|
|
35aacdb289 | ||
|
|
7a2802d870 | ||
|
|
8548d35620 | ||
|
|
07ef8e9c9a | ||
|
|
5963b425d1 | ||
|
|
7f5a67434e | ||
|
|
3abfeebd58 | ||
|
|
8b438c69db | ||
|
|
1d86653c9d | ||
|
|
79f3ea5cfa | ||
|
|
ba656e560b | ||
|
|
5a6d476490 | ||
|
|
16c35344d8 | ||
|
|
9f2ab7aa8f |
@@ -1,204 +0,0 @@
|
||||
name: Build PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!.forgejo/**'
|
||||
- '!*.yml'
|
||||
- '!*.config'
|
||||
- '!*.md'
|
||||
- '.forgejo/workflows/*.yml'
|
||||
|
||||
env:
|
||||
POWERSHELL_TELEMETRY_OPTOUT: 1
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
RELEASE: 0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.platform.name }} (${{ matrix.configuration }})
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
configuration: [Release]
|
||||
platform:
|
||||
- { name: win-x64, zip_os_name: win_x64 }
|
||||
#- { name: win-arm64, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, zip_os_name: linux_arm64 }
|
||||
#- { name: osx-x64, zip_os_name: osx_x64 }
|
||||
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Install GLI
|
||||
uses: actions/setup-gli@v1
|
||||
with:
|
||||
token: ${{ secrets.SETUP_GLI_TOKEN }}
|
||||
|
||||
- name: Install 7zip
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y 7zip
|
||||
|
||||
- name: Overwrite csc problem matcher
|
||||
run: echo "::add-matcher::.forgejo/csc.json"
|
||||
|
||||
- name: Get version info
|
||||
id: version_info
|
||||
run: |
|
||||
echo "result=$(gli get-next-version -c Canary -R)" >> $FORGEJO_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Change config filename
|
||||
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
if: forgejo.event_name == 'pull_request'
|
||||
|
||||
- name: 'Cache: ~/.nuget/packages'
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.nuget/packages
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c "${{ matrix.configuration }}" -p:Version="${{ steps.version_info.outputs.result }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:ExtraDefineConstants=DISABLE_UPDATER
|
||||
|
||||
- name: Test
|
||||
uses: actions/unstable-commands@v1
|
||||
with:
|
||||
commands: dotnet test --no-build -c "${{ matrix.configuration }}"
|
||||
timeout-minutes: 10
|
||||
retry-codes: 139
|
||||
if: matrix.platform.name != 'linux-arm64'
|
||||
|
||||
- name: Publish Ryujinx
|
||||
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.result }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained
|
||||
if: forgejo.event_name == 'pull_request'
|
||||
|
||||
- name: Packing Windows builds
|
||||
if: contains(matrix.platform.name, 'win')
|
||||
run: |
|
||||
7z a artifact/ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}.7z publish
|
||||
shell: bash
|
||||
|
||||
- name: Upload Ryujinx Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}
|
||||
path: artifact
|
||||
if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'win')
|
||||
|
||||
- name: Build AppImage
|
||||
if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux')
|
||||
run: |
|
||||
chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh
|
||||
|
||||
PLATFORM_NAME="${{ matrix.platform.name }}"
|
||||
|
||||
sudo apt update && sudo apt install -y zsync desktop-file-utils appstream libfuse2t64
|
||||
|
||||
mkdir -p tools
|
||||
export PATH="$PATH:$(readlink -f tools)"
|
||||
|
||||
# Setup appimagetool
|
||||
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x tools/appimagetool
|
||||
chmod +x distribution/linux/appimage/build-appimage.sh
|
||||
|
||||
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
|
||||
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
|
||||
ARCH_NAME=x64
|
||||
export ARCH=x86_64
|
||||
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
|
||||
ARCH_NAME=arm64
|
||||
export ARCH=aarch64
|
||||
else
|
||||
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
|
||||
shell: bash
|
||||
|
||||
- name: Upload Ryujinx AppImage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux')
|
||||
with:
|
||||
name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}-AppImage
|
||||
path: publish_appimage
|
||||
|
||||
build_macos:
|
||||
name: macOS Universal (${{ matrix.configuration }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
configuration: [ Release ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Setup LLVM 17
|
||||
run: |
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
chmod +x llvm.sh
|
||||
sudo ./llvm.sh 17
|
||||
|
||||
- name: Install GLI
|
||||
uses: actions/setup-gli@v1
|
||||
with:
|
||||
token: ${{ secrets.SETUP_GLI_TOKEN }}
|
||||
|
||||
- name: Install rcodesign
|
||||
run: |
|
||||
gli ghr -R indygreg/apple-platform-rs -p apple-codesign-*-x86_64-unknown-linux-musl.tar.gz -O apple-codesign.tar.gz
|
||||
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
|
||||
rm apple-codesign.tar.gz
|
||||
sudo mv rcodesign /usr/bin/rcodesign
|
||||
|
||||
- name: Get version info
|
||||
id: version_info
|
||||
run: |
|
||||
echo "result=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Change config filename
|
||||
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
if: forgejo.event_name == 'pull_request'
|
||||
|
||||
- name: 'Cache: ~/.nuget/packages'
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.nuget/packages
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }}
|
||||
|
||||
- name: Publish macOS Ryujinx
|
||||
run: |
|
||||
bash distribution/macos/create_macos_pr_build_ava.sh . publish_tmp publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.result }}" "${{ steps.version_info.outputs.git_short_hash }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
|
||||
shell: bash
|
||||
|
||||
- name: Upload Ryujinx artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-macos_universal
|
||||
path: "publish/*.tar.gz"
|
||||
if: forgejo.event_name == 'pull_request'
|
||||
86
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
86
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]"
|
||||
labels: bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: issue
|
||||
attributes:
|
||||
label: Description of the issue
|
||||
description: What's the issue you encountered?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: How can the issue be reproduced?
|
||||
placeholder: Describe each step as precisely as possible
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Log file
|
||||
description: "A log file will help our developers to better diagnose and fix the issue. UPLOAD THE FILE. DO NOT COPY AND PASTE THE FILE'S CONTENT."
|
||||
placeholder: Logs files can be found under "Logs" folder in Ryujinx program folder. They can also be accessed by opening Ryujinx, then going to File > Open Logs Folder. You can drag and drop the log on to the text area (do not copy paste).
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: OS
|
||||
placeholder: "e.g. Windows 10"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: ryujinx-version
|
||||
attributes:
|
||||
label: Ryujinx version
|
||||
placeholder: "e.g. 1.0.470"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: game-version
|
||||
attributes:
|
||||
label: Game version
|
||||
placeholder: "e.g. 1.1.1"
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: cpu
|
||||
attributes:
|
||||
label: CPU
|
||||
placeholder: "e.g. i7-6700"
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: gpu
|
||||
attributes:
|
||||
label: GPU
|
||||
placeholder: "e.g. NVIDIA RTX 2070"
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: ram
|
||||
attributes:
|
||||
label: RAM
|
||||
placeholder: "e.g. 16GB"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: mods
|
||||
attributes:
|
||||
label: List of applied mods
|
||||
placeholder: You can list applied mods here.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context?
|
||||
description: |
|
||||
- Additional info about your environment:
|
||||
- Any other information relevant to your issue.
|
||||
validations:
|
||||
required: false
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Ryujinx Discord
|
||||
url: https://discord.gg/N2FmfVc
|
||||
about: This is for development related issues. For support and technical issues, please come to our Discord server.
|
||||
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
31
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature for Ryujinx.
|
||||
title: "[Feature Request]"
|
||||
labels: enhancement
|
||||
body:
|
||||
- type: textarea
|
||||
id: overview
|
||||
attributes:
|
||||
label: Overview
|
||||
description: Include the basic, high-level concepts for this feature here.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: details
|
||||
attributes:
|
||||
label: Smaller details
|
||||
description: These may include specific methods of implementation etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: request
|
||||
attributes:
|
||||
label: Nature of request
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: Why would this feature be useful?
|
||||
validations:
|
||||
required: true
|
||||
26
.github/ISSUE_TEMPLATE/missing_cpu_instruction.yml
vendored
Normal file
26
.github/ISSUE_TEMPLATE/missing_cpu_instruction.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Missing CPU Instruction
|
||||
description: CPU Instruction is missing in Ryujinx.
|
||||
title: "[CPU]"
|
||||
labels: [cpu, not-implemented]
|
||||
body:
|
||||
- type: textarea
|
||||
id: instruction
|
||||
attributes:
|
||||
label: CPU instruction
|
||||
description: What CPU instruction is missing?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: name
|
||||
attributes:
|
||||
label: Instruction name
|
||||
description: Include the name from [armconverter.com](https://armconverter.com/?disasm) or [shell-storm.org](http://shell-storm.org/online/Online-Assembler-and-Disassembler/?arch=arm64&endianness=big&dis_with_raw=True&dis_with_ins=True) in the above code block
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: required
|
||||
attributes:
|
||||
label: Required by
|
||||
description: Add links to the [compatibility list page(s)](https://github.com/Ryujinx/Ryujinx-Games-List/issues) of the game(s) that require this instruction.
|
||||
validations:
|
||||
required: true
|
||||
25
.github/ISSUE_TEMPLATE/missing_service_call.yml
vendored
Normal file
25
.github/ISSUE_TEMPLATE/missing_service_call.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Missing Service Call
|
||||
description: Service call is missing in Ryujinx.
|
||||
labels: not-implemented
|
||||
body:
|
||||
- type: textarea
|
||||
id: instruction
|
||||
attributes:
|
||||
label: Service call
|
||||
description: What service call is missing?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: name
|
||||
attributes:
|
||||
label: Service description
|
||||
description: Include the description/explanation from [Switchbrew](https://switchbrew.org/w/index.php?title=Services_API) and/or [SwIPC](https://reswitched.github.io/SwIPC/) in the above code block
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: required
|
||||
attributes:
|
||||
label: Required by
|
||||
description: Add links to the [compatibility list page(s)](https://github.com/Ryujinx/Ryujinx-Games-List/issues) of the game(s) that require this service.
|
||||
validations:
|
||||
required: true
|
||||
19
.github/ISSUE_TEMPLATE/missing_shader_instruction.yml
vendored
Normal file
19
.github/ISSUE_TEMPLATE/missing_shader_instruction.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Missing Shader Instruction
|
||||
description: Shader Instruction is missing in Ryujinx.
|
||||
title: "[GPU]"
|
||||
labels: [gpu, not-implemented]
|
||||
body:
|
||||
- type: textarea
|
||||
id: instruction
|
||||
attributes:
|
||||
label: Shader instruction
|
||||
description: What shader instruction is missing?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: required
|
||||
attributes:
|
||||
label: Required by
|
||||
description: Add links to the [compatibility list page(s)](https://github.com/Ryujinx/Ryujinx-Games-List/issues) of the game(s) that require this instruction.
|
||||
validations:
|
||||
required: true
|
||||
0
.forgejo/csc.json → .github/csc.json
vendored
0
.forgejo/csc.json → .github/csc.json
vendored
22
.forgejo/labeler.yml → .github/labeler.yml
vendored
22
.forgejo/labeler.yml → .github/labeler.yml
vendored
@@ -10,10 +10,6 @@ gpu:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['src/Ryujinx.Graphics.*/**', 'src/Spv.Generator/**', 'src/Ryujinx.ShaderTools/**']
|
||||
|
||||
input:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['src/Ryujinx.Input*/**', 'src/Ryujinx/UI/Views/Input/**']
|
||||
|
||||
'graphics-backend:opengl':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'src/Ryujinx.Graphics.OpenGL/**'
|
||||
@@ -22,17 +18,17 @@ input:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['src/Ryujinx.Graphics.Vulkan/**', 'src/Spv.Generator/**']
|
||||
|
||||
'graphics-backend:metal':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['src/Ryujinx.Graphics.Metal/**', 'src/Ryujinx.Graphics.Metal.SharpMetalExtensions/**']
|
||||
|
||||
gui:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.LocaleGenerator/**']
|
||||
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**']
|
||||
|
||||
'horizon/hle':
|
||||
horizon:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['src/Ryujinx.HLE/**', 'src/Ryujinx.HLE.Generators/**', 'src/Ryujinx.Horizon/**']
|
||||
|
||||
i18n:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['assets/**/*.json', 'src/Ryujinx.UI.LocaleGenerator/**']
|
||||
- any-glob-to-any-file: ['src/Ryujinx.HLE/**', 'src/Ryujinx.Horizon/**']
|
||||
|
||||
kernel:
|
||||
- changed-files:
|
||||
@@ -40,7 +36,7 @@ kernel:
|
||||
|
||||
infra:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['.forgejo/**', 'distribution/**', 'Directory.Packages.props', 'src/Ryujinx.BuildValidationTasks/**']
|
||||
- any-glob-to-any-file: ['.github/**', 'distribution/**', 'Directory.Packages.props', 'src/Ryujinx.BuildValidationTasks/**']
|
||||
|
||||
documentation:
|
||||
- changed-files:
|
||||
@@ -48,4 +44,4 @@ documentation:
|
||||
|
||||
ldn:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['src/Ryujinx.HLE/HOS/Services/Ldn/**', 'src/Ryujinx/UI/Windows/LdnGamesListWindow.*', 'src/Ryujinx/UI/ViewModels/LdnGamesListViewModel.cs']
|
||||
- any-glob-to-any-file: 'src/Ryujinx.HLE/HOS/Services/Ldn/**'
|
||||
168
.github/workflows/build.yml
vendored
Normal file
168
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
name: Build job
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
POWERSHELL_TELEMETRY_OPTOUT: 1
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||
RYUJINX_BASE_VERSION: "1.2.0"
|
||||
RELEASE: 0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.platform.name }} (${{ matrix.configuration }})
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
configuration: [Debug, Release]
|
||||
platform:
|
||||
- { name: win-x64, os: windows-latest, zip_os_name: win_x64 }
|
||||
- { name: win-arm64, os: windows-latest, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
|
||||
- { name: osx-x64, os: macos-13, zip_os_name: osx_x64 }
|
||||
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Overwrite csc problem matcher
|
||||
run: echo "::add-matcher::.github/csc.json"
|
||||
|
||||
- name: Get git short hash
|
||||
id: git_short_hash
|
||||
run: echo "result=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Change config filename
|
||||
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
- name: Change config filename for macOS
|
||||
run: sed -r -i '' 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os == 'macos-13'
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c "${{ matrix.configuration }}" -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER
|
||||
|
||||
- name: Test
|
||||
uses: TSRBerry/unstable-commands@v1
|
||||
with:
|
||||
commands: dotnet test --no-build -c "${{ matrix.configuration }}"
|
||||
timeout-minutes: 10
|
||||
retry-codes: 139
|
||||
if: matrix.platform.name != 'linux-arm64'
|
||||
|
||||
- name: Publish Ryujinx
|
||||
run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ env.RYUJINX_BASE_VERSION }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.git_short_hash.outputs.result }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
- name: Set executable bit
|
||||
run: |
|
||||
chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
||||
|
||||
- name: Build AppImage
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
||||
run: |
|
||||
PLATFORM_NAME="${{ matrix.platform.name }}"
|
||||
|
||||
sudo apt install -y zsync desktop-file-utils appstream
|
||||
|
||||
mkdir -p tools
|
||||
export PATH="$PATH:$(readlink -f tools)"
|
||||
|
||||
# Setup appimagetool
|
||||
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
chmod +x tools/appimagetool
|
||||
chmod +x distribution/linux/appimage/build-appimage.sh
|
||||
|
||||
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
|
||||
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
|
||||
ARCH_NAME=x64
|
||||
export ARCH=x86_64
|
||||
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
|
||||
ARCH_NAME=arm64
|
||||
export ARCH=aarch64
|
||||
else
|
||||
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
|
||||
BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
|
||||
shell: bash
|
||||
|
||||
- name: Upload Ryujinx artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}
|
||||
path: publish
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||
|
||||
- name: Upload Ryujinx (AppImage) artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
||||
with:
|
||||
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}-AppImage
|
||||
path: publish_appimage
|
||||
|
||||
build_macos:
|
||||
name: macOS Universal (${{ matrix.configuration }})
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
matrix:
|
||||
configuration: [ Debug, Release ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Setup LLVM 17
|
||||
run: |
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
chmod +x llvm.sh
|
||||
sudo ./llvm.sh 17
|
||||
|
||||
- name: Install rcodesign
|
||||
run: |
|
||||
mkdir -p $HOME/.bin
|
||||
gh release download -R indygreg/apple-platform-rs -O apple-codesign.tar.gz -p 'apple-codesign-*-x86_64-unknown-linux-musl.tar.gz'
|
||||
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
|
||||
rm apple-codesign.tar.gz
|
||||
mv rcodesign $HOME/.bin/
|
||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get git short hash
|
||||
id: git_short_hash
|
||||
run: echo "result=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Change config filename
|
||||
run: sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/PRConfig\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
- name: Publish macOS Ryujinx
|
||||
run: |
|
||||
./distribution/macos/create_macos_build_ava.sh . publish_tmp publish ./distribution/macos/entitlements.xml "${{ env.RYUJINX_BASE_VERSION }}" "${{ steps.git_short_hash.outputs.result }}" "${{ matrix.configuration }}" "-p:ExtraDefineConstants=DISABLE_UPDATER"
|
||||
|
||||
- name: Upload Ryujinx artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal
|
||||
path: "publish/*.tar.gz"
|
||||
if: github.event_name == 'pull_request'
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.forgejo/**'
|
||||
- '.github/**'
|
||||
- 'docs/**'
|
||||
- 'assets/**'
|
||||
- '*.yml'
|
||||
@@ -25,41 +25,44 @@ env:
|
||||
jobs:
|
||||
release:
|
||||
name: Release for ${{ matrix.platform.name }}
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ${{ matrix.platform.os }}
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 }
|
||||
#- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
|
||||
- { name: win-x64, os: ubuntu-latest, zip_os_name: win_x64 }
|
||||
#- { name: win-arm64, os: ubuntu-latest, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v5
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
- name: Overwrite csc problem matcher
|
||||
run: echo "::add-matcher::.forgejo/csc.json"
|
||||
|
||||
- name: Install GLI
|
||||
uses: actions/setup-gli@v1
|
||||
with:
|
||||
token: ${{ secrets.SETUP_GLI_TOKEN }}
|
||||
run: echo "::add-matcher::.github/csc.json"
|
||||
|
||||
- name: Install 7zip
|
||||
run: |
|
||||
sudo apt update && sudo apt install -y 7zip
|
||||
sudo apt install -y 7zip
|
||||
|
||||
- name: Install gli
|
||||
run: |
|
||||
mkdir -p $HOME/.bin
|
||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||
chmod +x gli
|
||||
mv gli $HOME/.bin/
|
||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version info
|
||||
id: version_info
|
||||
run: |
|
||||
echo "build_version=$(gli get-next-version -c Canary -R)" >> $FORGEJO_OUTPUT
|
||||
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $FORGEJO_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
|
||||
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Configure for release
|
||||
@@ -84,9 +87,12 @@ jobs:
|
||||
pushd publish
|
||||
rm libarmeilleure-jitsupport.dylib
|
||||
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
|
||||
7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.7z ../publish
|
||||
popd
|
||||
|
||||
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Packing Linux builds
|
||||
if: contains(matrix.platform.name, 'linux')
|
||||
@@ -95,8 +101,9 @@ jobs:
|
||||
rm libarmeilleure-jitsupport.dylib
|
||||
chmod +x Ryujinx.sh Ryujinx
|
||||
tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
|
||||
tar -cJvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.xz ../publish
|
||||
popd
|
||||
|
||||
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
|
||||
shell: bash
|
||||
|
||||
- name: Build AppImage (Linux)
|
||||
@@ -105,7 +112,7 @@ jobs:
|
||||
BUILD_VERSION="${{ steps.version_info.outputs.build_version }}"
|
||||
PLATFORM_NAME="${{ matrix.platform.name }}"
|
||||
|
||||
sudo apt update && sudo apt install -y zsync desktop-file-utils appstream libfuse2t64
|
||||
sudo apt install -y zsync desktop-file-utils appstream
|
||||
|
||||
mkdir -p tools
|
||||
export PATH="$PATH:$(readlink -f tools)"
|
||||
@@ -132,28 +139,17 @@ jobs:
|
||||
pushd publish_appimage
|
||||
mv Ryujinx.AppImage ../release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
|
||||
popd
|
||||
|
||||
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-canary-$BUILD_VERSION-$ARCH_NAME.AppImage
|
||||
shell: bash
|
||||
|
||||
- name: Create release
|
||||
uses: actions/create-release@v1
|
||||
with:
|
||||
name: "Canary ${{ steps.version_info.outputs.build_version }}"
|
||||
body: "**Full Changelog:** [`${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}`](https://git.ryujinx.app/projects/Ryubing/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
|
||||
repository: "Ryubing/Canary"
|
||||
token: ${{ secrets.RELEASER_TOKEN }}
|
||||
tag_name: ${{ steps.version_info.outputs.build_version }}
|
||||
files: |-
|
||||
release_output/**
|
||||
|
||||
macos_release:
|
||||
name: Release MacOS universal
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v5
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
@@ -163,24 +159,33 @@ jobs:
|
||||
chmod +x llvm.sh
|
||||
sudo ./llvm.sh 17
|
||||
|
||||
- name: Install GLI
|
||||
uses: actions/setup-gli@v1
|
||||
with:
|
||||
token: ${{ secrets.SETUP_GLI_TOKEN }}
|
||||
- name: Install gli
|
||||
run: |
|
||||
mkdir -p $HOME/.bin
|
||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||
chmod +x gli
|
||||
mv gli $HOME/.bin/
|
||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install rcodesign
|
||||
run: |
|
||||
gli ghr -R indygreg/apple-platform-rs -p apple-codesign-*-x86_64-unknown-linux-musl.tar.gz -O apple-codesign.tar.gz
|
||||
mkdir -p $HOME/.bin
|
||||
gh release download -R indygreg/apple-platform-rs -O apple-codesign.tar.gz -p 'apple-codesign-*-x86_64-unknown-linux-musl.tar.gz'
|
||||
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
|
||||
rm apple-codesign.tar.gz
|
||||
mv rcodesign /usr/bin/rcodesign
|
||||
mv rcodesign $HOME/.bin/
|
||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version info
|
||||
id: version_info
|
||||
run: |
|
||||
echo "build_version=$(gli get-next-version -c Canary -R)" >> $FORGEJO_OUTPUT
|
||||
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $FORGEJO_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
|
||||
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Configure for release
|
||||
@@ -196,53 +201,46 @@ jobs:
|
||||
- name: Publish macOS Ryujinx
|
||||
run: |
|
||||
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish_ava ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1
|
||||
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r 5 -p publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
|
||||
|
||||
- name: Create release
|
||||
uses: actions/create-release@v1
|
||||
with:
|
||||
name: "Canary ${{ steps.version_info.outputs.build_version }}"
|
||||
body: "**Full Changelog:** [`${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}`](https://git.ryujinx.app/projects/Ryubing/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
|
||||
repository: "Ryubing/Canary"
|
||||
token: ${{ secrets.RELEASER_TOKEN }}
|
||||
tag_name: ${{ steps.version_info.outputs.build_version }}
|
||||
files: |-
|
||||
publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
|
||||
|
||||
post_ci:
|
||||
name: Post CI Steps
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||
create_gitlab_release:
|
||||
name: Create GitLab Release
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- macos_release
|
||||
- release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install GLI
|
||||
uses: actions/setup-gli@v1
|
||||
with:
|
||||
token: ${{ secrets.SETUP_GLI_TOKEN }}
|
||||
- name: Install gli
|
||||
run: |
|
||||
mkdir -p $HOME/.bin
|
||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||
chmod +x gli
|
||||
mv gli $HOME/.bin/
|
||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version info
|
||||
id: version_info
|
||||
run: |
|
||||
echo "build_version=$(gli get-next-version -c Canary -R)" >> $FORGEJO_OUTPUT
|
||||
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $FORGEJO_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
|
||||
echo "build_version=$(gli get-next-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||
echo "prev_build_version=$(gli get-current-version -c Canary -R)" >> $GITHUB_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Create tag
|
||||
run: |
|
||||
gli create-tag -T ${{ secrets.RELEASER_TOKEN }} -P projects/Ryubing -n Canary-${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }}
|
||||
|
||||
- name: Link to actual source archives for Canary
|
||||
run: |
|
||||
gli canary-release -T ${{ secrets.RELEASER_TOKEN }} -P Ryubing/Canary -r ${{ steps.version_info.outputs.build_version }}
|
||||
gli create-tag -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Canary-${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }}
|
||||
|
||||
- name: Create release
|
||||
run: |
|
||||
gli create-release-from-generic-package-files -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -n Ryubing-Canary -v ${{ steps.version_info.outputs.build_version }} -r main -t "Canary ${{ steps.version_info.outputs.build_version }}" -b "**Full Changelog:** [${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}](https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }})"
|
||||
|
||||
- name: Send notification webhook
|
||||
run: |
|
||||
gli send-update-message -T ${{ secrets.RELEASER_TOKEN }} -P Ryubing/Canary -t ${{ steps.version_info.outputs.build_version }} -c FF4500 -w ${{ secrets.CANARY_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
|
||||
gli send-update-message -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/canary -t ${{ steps.version_info.outputs.build_version }} -c FF4500 -w ${{ secrets.CANARY_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
|
||||
|
||||
- name: Notify update server of new builds
|
||||
run: |
|
||||
25
.github/workflows/checks.yml
vendored
Normal file
25
.github/workflows/checks.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Build PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**'
|
||||
- '!.github/**'
|
||||
- '!*.yml'
|
||||
- '!*.config'
|
||||
- '!*.md'
|
||||
- '.github/workflows/*.yml'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
checks: write
|
||||
|
||||
concurrency:
|
||||
group: pr-checks-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pr_build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
secrets: inherit
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -14,13 +18,11 @@ jobs:
|
||||
with:
|
||||
# Ensure we pin the source origin as pull_request_target run under forks.
|
||||
fetch-depth: 0
|
||||
repository: projects/Ryubing
|
||||
repository: GreemDev/Ryujinx
|
||||
ref: master
|
||||
|
||||
- name: Update labels based on changes
|
||||
uses: actions/labeler@v6
|
||||
uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.LABELER_TOKEN }}
|
||||
configuration-path: .forgejo/labeler.yml
|
||||
sync-labels: true
|
||||
dot: true
|
||||
@@ -19,20 +19,18 @@ env:
|
||||
jobs:
|
||||
release:
|
||||
name: Release for ${{ matrix.platform.name }}
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ${{ matrix.platform.os }}
|
||||
runs-on: ${{ matrix.platform.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 }
|
||||
#- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
|
||||
- { name: win-x64, os: ubuntu-latest, zip_os_name: win_x64 }
|
||||
#- { name: win-arm64, os: ubuntu-latest, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, os: ubuntu-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ubuntu-latest, zip_os_name: linux_arm64 }
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v5
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
@@ -43,21 +41,26 @@ jobs:
|
||||
run: |
|
||||
sudo apt install -y 7zip
|
||||
|
||||
- name: Install GLI
|
||||
uses: actions/setup-gli@v1
|
||||
with:
|
||||
token: ${{ secrets.SETUP_GLI_TOKEN }}
|
||||
- name: Install gli
|
||||
run: |
|
||||
mkdir -p $HOME/.bin
|
||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||
chmod +x gli
|
||||
mv gli $HOME/.bin/
|
||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version info
|
||||
id: version_info
|
||||
run: |
|
||||
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
|
||||
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "build_version=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
|
||||
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Configure for release
|
||||
@@ -81,9 +84,12 @@ jobs:
|
||||
pushd publish
|
||||
rm libarmeilleure-jitsupport.dylib
|
||||
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
|
||||
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.7z ../publish
|
||||
popd
|
||||
|
||||
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Packing Linux builds
|
||||
if: contains(matrix.platform.name, 'linux')
|
||||
@@ -92,9 +98,12 @@ jobs:
|
||||
rm libarmeilleure-jitsupport.dylib
|
||||
chmod +x Ryujinx.sh Ryujinx
|
||||
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish
|
||||
tar -cJvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.xz ../publish
|
||||
popd
|
||||
|
||||
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build AppImage (Linux)
|
||||
if: contains(matrix.platform.name, 'linux')
|
||||
@@ -129,27 +138,17 @@ jobs:
|
||||
pushd publish_appimage
|
||||
mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
|
||||
popd
|
||||
shell: bash
|
||||
|
||||
- name: Create release
|
||||
uses: actions/create-release@v1
|
||||
with:
|
||||
name: "${{ steps.version_info.outputs.build_version }}"
|
||||
repository: "projects/Ryubing"
|
||||
token: ${{ secrets.RELEASER_TOKEN }}
|
||||
tag_name: ${{ steps.version_info.outputs.build_version }}
|
||||
files: |-
|
||||
release_output/**
|
||||
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage
|
||||
shell: bash
|
||||
|
||||
macos_release:
|
||||
name: Release MacOS universal
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v5
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
global-json-file: global.json
|
||||
|
||||
@@ -159,28 +158,37 @@ jobs:
|
||||
chmod +x llvm.sh
|
||||
sudo ./llvm.sh 17
|
||||
|
||||
- name: Install GLI
|
||||
uses: actions/setup-gli@v1
|
||||
with:
|
||||
token: ${{ secrets.SETUP_GLI_TOKEN }}
|
||||
- name: Install gli
|
||||
run: |
|
||||
mkdir -p $HOME/.bin
|
||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||
chmod +x gli
|
||||
mv gli $HOME/.bin/
|
||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install rcodesign
|
||||
run: |
|
||||
gli ghr -R indygreg/apple-platform-rs -p apple-codesign-*-x86_64-unknown-linux-musl.tar.gz -O apple-codesign.tar.gz
|
||||
mkdir -p $HOME/.bin
|
||||
gh release download -R indygreg/apple-platform-rs -O apple-codesign.tar.gz -p 'apple-codesign-*-x86_64-unknown-linux-musl.tar.gz'
|
||||
tar -xzvf apple-codesign.tar.gz --wildcards '*/rcodesign' --strip-components=1
|
||||
rm apple-codesign.tar.gz
|
||||
mv rcodesign /usr/bin/rcodesign
|
||||
mv rcodesign $HOME/.bin/
|
||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version info
|
||||
id: version_info
|
||||
run: |
|
||||
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
|
||||
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "build_version=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
|
||||
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Configure for release
|
||||
@@ -193,20 +201,12 @@ jobs:
|
||||
|
||||
- name: Publish macOS Ryujinx
|
||||
run: |
|
||||
bash 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
|
||||
|
||||
- name: Create release
|
||||
uses: actions/create-release@v1
|
||||
with:
|
||||
name: "${{ steps.version_info.outputs.build_version }}"
|
||||
repository: "projects/Ryubing"
|
||||
token: ${{ secrets.RELEASER_TOKEN }}
|
||||
tag_name: ${{ steps.version_info.outputs.build_version }}
|
||||
files: |-
|
||||
publish_ava/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
|
||||
|
||||
post_ci:
|
||||
name: Post-CI Steps
|
||||
./distribution/macos/create_macos_build_ava.sh . publish_tmp_ava publish ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0
|
||||
|
||||
gli upload-generic-package -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r 5 -p publish/ryujinx-${{ steps.version_info.outputs.build_version }}-macos_universal.app.tar.gz
|
||||
|
||||
create_gitlab_release:
|
||||
name: Create GitLab Release
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- macos_release
|
||||
@@ -214,26 +214,36 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install GLI
|
||||
uses: actions/setup-gli@v1
|
||||
with:
|
||||
token: ${{ secrets.SETUP_GLI_TOKEN }}
|
||||
- name: Install gli
|
||||
run: |
|
||||
mkdir -p $HOME/.bin
|
||||
gh release download -R GreemDev/GLI -O gli -p 'gli-linux-x64'
|
||||
chmod +x gli
|
||||
mv gli $HOME/.bin/
|
||||
echo "$HOME/.bin" >> $GITHUB_PATH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get version info
|
||||
id: version_info
|
||||
run: |
|
||||
if [ '${{ inputs.is_bugfix_release }}' == 'false' ]; then
|
||||
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "build_version=$(gli get-next-version -m -c Stable -R)" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "build_version=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "build_version=$(gli get-next-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $FORGEJO_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT
|
||||
echo "prev_build_version=$(gli get-current-version -c Stable -R)" >> $GITHUB_OUTPUT
|
||||
echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT
|
||||
echo "commit_message=$(git log -1 --pretty=%B)" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
|
||||
- name: Create release
|
||||
run: |
|
||||
gli create-release-from-generic-package-files -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -n Ryubing -v ${{ steps.version_info.outputs.build_version }} -r ${{ steps.version_info.outputs.git_short_hash }} -t "${{ steps.version_info.outputs.build_version }}" -b "msd:${{ steps.version_info.outputs.build_version }}"
|
||||
|
||||
- name: Send notification webhook
|
||||
run: |
|
||||
gli send-update-message -T ${{ secrets.RELEASER_TOKEN }} -P projects/Ryubing -t ${{ steps.version_info.outputs.build_version }} -c 32cd32 -w ${{ secrets.STABLE_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
|
||||
gli send-update-message -T ${{ secrets.GITLAB_TOKEN }} -P ryubing/ryujinx -t ${{ steps.version_info.outputs.build_version }} -c 32cd32 -w ${{ secrets.STABLE_DISCORD_WEBHOOK }} -i https://avatars.githubusercontent.com/u/192939710?s=200&v=4
|
||||
|
||||
- name: Notify update server of new builds
|
||||
run: |
|
||||
@@ -3,27 +3,26 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="11.3.12" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.12" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.12" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.12" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.47.4" />
|
||||
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.9.4" />
|
||||
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.9.4" />
|
||||
<PackageVersion Include="Avalonia" Version="11.3.6" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.6" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.6" />
|
||||
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.6.2" />
|
||||
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.6.2" />
|
||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
|
||||
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="Projektanker.Icons.Avalonia" Version="9.6.2" />
|
||||
<PackageVersion Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.6.2" />
|
||||
<PackageVersion Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.6.2" />
|
||||
<PackageVersion Include="ppy.SDL3-CS" Version="2026.320.0" />
|
||||
<PackageVersion Include="ppy.SDL3-CS" Version="2025.920.0" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageVersion Include="Concentus" Version="2.2.2" />
|
||||
<PackageVersion Include="DiscordRichPresence" Version="1.6.1.70" />
|
||||
<PackageVersion Include="DynamicData" Version="9.4.1" />
|
||||
<PackageVersion Include="FluentAvaloniaUI" Version="2.5.0" />
|
||||
<PackageVersion Include="FluentAvaloniaUI.NoAnim" Version="2.4.0-build3" />
|
||||
<PackageVersion Include="Humanizer" Version="2.14.1" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||
@@ -42,13 +41,14 @@
|
||||
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.2-build3" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.129" />
|
||||
<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="2.0.6" />
|
||||
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="1.0.44" />
|
||||
<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" />
|
||||
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.22.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.22.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.22.0" />
|
||||
@@ -56,6 +56,7 @@
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||
<PackageVersion Include="SPB" Version="0.0.4-build32" />
|
||||
<PackageVersion Include="System.IO.Hashing" Version="9.0.2" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.2" />
|
||||
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
24
README.md
24
README.md
@@ -7,8 +7,8 @@
|
||||
|
||||
# Ryujinx
|
||||
|
||||
[](https://update.ryujinx.app/latest/stable)
|
||||
[](https://update.ryujinx.app/latest/canary)
|
||||
[](https://update.ryujinx.app/latest/stable)
|
||||
[](https://update.ryujinx.app/latest/canary)
|
||||
<br>
|
||||
<a href="https://discord.gg/PEuzjrFXUA">
|
||||
<img src="https://img.shields.io/discord/1294443224030511104?color=5865F2&label=Ryubing&logo=discord&logoColor=white" alt="Discord">
|
||||
@@ -21,7 +21,7 @@
|
||||
Ryujinx is an open-source Nintendo Switch emulator, originally created by gdkchan, written in C#.
|
||||
This emulator aims at providing excellent accuracy and performance, a user-friendly interface and consistent builds.
|
||||
It was written from scratch and development on the project began in September 2017.
|
||||
Ryujinx is available on a self-managed <a href="https://github.com/Ryubing/forgejo" target="_blank">modified</a> <a href="https://forgejo.org/" target="_blank">Forgejo</a> instance under the <a href="https://git.ryujinx.app/projects/Ryubing/src/branch/master/LICENSE.txt" target="_blank">MIT license</a>.
|
||||
Ryujinx is available on a self-managed GitLab instance under the <a href="https://git.ryujinx.app/ryubing/ryujinx/-/blob/master/LICENSE.txt?ref_type=heads" target="_blank">MIT license</a>.
|
||||
<br />
|
||||
</p>
|
||||
<p align="center">
|
||||
@@ -31,11 +31,11 @@
|
||||
<br>
|
||||
This is not a Ryujinx revival project. This is not a Phoenix project.
|
||||
<br>
|
||||
Guides and documentation can be found on the <a href="https://git.ryujinx.app/projects/Ryubing/wiki/Home">Wiki tab</a>.
|
||||
Guides and documentation can be found on the <a href="https://git.ryujinx.app/groups/ryubing/-/wikis/home">Wiki tab</a>.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://git.ryujinx.app/projects/Ryubing/raw/branch/master/docs/shell.png" alt="Ryujinx example">
|
||||
<img src="https://git.ryujinx.app/ryubing/ryujinx/-/raw/master/docs/shell.png?ref_type=heads&inline=false" alt="Ryujinx example">
|
||||
</p>
|
||||
|
||||
## Usage
|
||||
@@ -49,17 +49,17 @@ Stable builds are made every so often, based on the `master` branch, that then g
|
||||
These stable builds exist so that the end user can get a more **enjoyable and stable experience**.
|
||||
They are released every month or so, to ensure consistent updates, while not being an annoying amount of individual updates to download over the course of that month.
|
||||
|
||||
You can find the stable releases [here](https://git.ryujinx.app/projects/Ryubing/releases).
|
||||
You can find the stable releases [here](https://git.ryujinx.app/ryubing/ryujinx/-/releases).
|
||||
|
||||
Canary builds are compiled automatically for each commit on the `master` branch.
|
||||
While we strive to ensure optimal stability and performance prior to pushing an update, these builds **may be unstable or completely broken**.
|
||||
These canary builds are only recommended for experienced users.
|
||||
|
||||
You can find the canary releases [here](https://git.ryujinx.app/Ryubing/Canary/releases).
|
||||
You can find the canary releases [here](https://git.ryujinx.app/ryubing/canary/-/releases).
|
||||
|
||||
## Documentation
|
||||
|
||||
If you are planning to contribute or just want to learn more about this project please read through our [documentation](https://git.ryujinx.app/projects/Ryubing/src/branch/master/docs/README.md).
|
||||
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
|
||||
|
||||
## Features
|
||||
|
||||
@@ -105,13 +105,13 @@ If you are planning to contribute or just want to learn more about this project
|
||||
|
||||
## License
|
||||
|
||||
This software is licensed under the terms of the [MIT license](https://git.ryujinx.app/projects/Ryubing/src/branch/master/LICENSE.txt).
|
||||
This software is licensed under the terms of the [MIT license](LICENSE.txt).
|
||||
This project makes use of code authored by the libvpx project, licensed under BSD and the ffmpeg project, licensed under LGPLv3.
|
||||
See [LICENSE.txt](https://git.ryujinx.app/projects/Ryubing/src/branch/master/LICENSE.txt) and [THIRDPARTY.md](https://git.ryujinx.app/projects/Ryubing/src/branch/master/distribution/legal/THIRDPARTY.md) for more details.
|
||||
See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY.md) for more details.
|
||||
|
||||
## Credits
|
||||
|
||||
- [LibHac](https://git.ryujinx.app/projects/LibHac) is used for our file-system.
|
||||
- [LibHac](https://git.ryujinx.app/ryubing/libhac) is used for our file-system.
|
||||
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
|
||||
- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
|
||||
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.
|
||||
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.
|
||||
16
Ryujinx.sln
16
Ryujinx.sln
@@ -86,11 +86,11 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.forgejo\workflows\build.yml = .forgejo\workflows\build.yml
|
||||
.forgejo\workflows\canary.yml = .forgejo\workflows\canary.yml
|
||||
.github\workflows\build.yml = .github\workflows\build.yml
|
||||
.github\workflows\canary.yml = .github\workflows\canary.yml
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
Directory.Build.props = Directory.Build.props
|
||||
.forgejo\workflows\release.yml = .forgejo\workflows\release.yml
|
||||
.github\workflows\release.yml = .github\workflows\release.yml
|
||||
nuget.config = nuget.config
|
||||
EndProjectSection
|
||||
EndProject
|
||||
@@ -573,16 +573,6 @@ Global
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "启动 RenderDoc 帧捕获",
|
||||
"zh_TW": "啟動 RenderDoc 畫格擷取"
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -47,7 +47,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "结束 RenderDoc 帧捕获",
|
||||
"zh_TW": "停止 RenderDoc 畫格擷取"
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -72,7 +72,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "丢弃 RenderDoc 帧捕获",
|
||||
"zh_TW": "捨棄 RenderDoc 畫格擷取"
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -97,7 +97,7 @@
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。",
|
||||
"zh_TW": "停止正在執行的 RenderDoc 畫格擷取,且立即捨棄其結果。"
|
||||
"zh_TW": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
854
assets/Locales/UserProfiles.json
Normal file
854
assets/Locales/UserProfiles.json
Normal file
@@ -0,0 +1,854 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "MenuBarOptions_OpenUserProfiles",
|
||||
"Translations": {
|
||||
"ar_SA": "_ملفات المستخدمين",
|
||||
"de_DE": "_Benutzerprofile",
|
||||
"el_GR": "_Προφίλ Χρηστών",
|
||||
"en_US": "_User Profiles",
|
||||
"es_ES": "_Perfiles de Usuario",
|
||||
"fr_FR": "_Profils d'Utilisateurs",
|
||||
"he_IL": "_פרופילי משתמש",
|
||||
"it_IT": "_Profili utent",
|
||||
"ja_JP": "ユーザプロファイル(_M)",
|
||||
"ko_KR": "사용자 프로필(_M)",
|
||||
"no_NO": "_Brukerprofiler",
|
||||
"pl_PL": "_Profile użytkowników",
|
||||
"pt_BR": "_Perfis de usuário",
|
||||
"ru_RU": "_Учётные записи",
|
||||
"sv_SE": "_Användarprofiler",
|
||||
"th_TH": "_โปรไฟล์ผู้ใช้งาน",
|
||||
"tr_TR": "_Kullanıcı Profilleri",
|
||||
"uk_UA": "_Профілі користувачів",
|
||||
"zh_CN": "用户配置文件(_M)",
|
||||
"zh_TW": "使用者設定檔(_M)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "WindowTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "ملفات المستخدمين",
|
||||
"de_DE": "Benutzerprofile",
|
||||
"el_GR": "Προφίλ Χρηστών",
|
||||
"en_US": "User Profiles",
|
||||
"es_ES": "Perfiles de Usuario",
|
||||
"fr_FR": "Profils d'Utilisateurs",
|
||||
"he_IL": "פרופילי משתמש",
|
||||
"it_IT": "Profili utent",
|
||||
"ja_JP": "ユーザプロファイル",
|
||||
"ko_KR": "사용자 프로필",
|
||||
"no_NO": "Brukerprofiler",
|
||||
"pl_PL": "Profile użytkowników",
|
||||
"pt_BR": "Perfis de usuário",
|
||||
"ru_RU": "Учётные записи",
|
||||
"sv_SE": "Användarprofiler",
|
||||
"th_TH": "โปรไฟล์ผู้ใช้งาน",
|
||||
"tr_TR": "Kullanıcı Profilleri",
|
||||
"uk_UA": "Профілі користувачів",
|
||||
"zh_CN": "用户配置文件",
|
||||
"zh_TW": "使用者設定檔"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves",
|
||||
"Translations": {
|
||||
"ar_SA": "عمليات الحفظ",
|
||||
"de_DE": "Speicherstände",
|
||||
"el_GR": "Αποθηκεύσεις",
|
||||
"en_US": "Saves",
|
||||
"es_ES": "Partidas",
|
||||
"fr_FR": "Sauvegardes",
|
||||
"he_IL": "שמירות",
|
||||
"it_IT": "Salvataggi",
|
||||
"ja_JP": "セーブデータ",
|
||||
"ko_KR": "저장",
|
||||
"no_NO": "Lagringer",
|
||||
"pl_PL": "Zapisy",
|
||||
"pt_BR": "Salvamentos",
|
||||
"ru_RU": "Сохранения",
|
||||
"sv_SE": "Sparningar",
|
||||
"th_TH": "บันทึก",
|
||||
"tr_TR": "Kayıtlar",
|
||||
"uk_UA": "Збереження",
|
||||
"zh_CN": "存档",
|
||||
"zh_TW": "存檔"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DeleteSaveNote",
|
||||
"Translations": {
|
||||
"ar_SA": "هل حذف بيانات حفظ المستخدم لهذه اللعبة؟",
|
||||
"de_DE": "Löschen Sie die gespeicherten Spielstände dieses Spiels?",
|
||||
"el_GR": "Διαγραφή των δεδομένων αποθήκευσης αυτού του παιχνιδιού;",
|
||||
"en_US": "Delete this game's save data?",
|
||||
"es_ES": "¿Eliminar los datos de guardado de este juego?",
|
||||
"fr_FR": "Supprimer les données de sauvegarde de ce jeu ?",
|
||||
"he_IL": "האם למחוק את נתוני השמירה של המשחק הזה?",
|
||||
"it_IT": "Eliminare i dati di salvataggio di questo gioco?",
|
||||
"ja_JP": "このゲームのセーブデータを削除しますか?",
|
||||
"ko_KR": "이 게임의 저장 데이터를 삭제하시겠습니까?",
|
||||
"no_NO": "Slette lagrede data for dette spillet?",
|
||||
"pl_PL": "Usunąć dane zapisu dla tej gry?",
|
||||
"pt_BR": "Excluir os dados salvos deste jogo?",
|
||||
"ru_RU": "Удалить данные сохранений для этой игры?",
|
||||
"sv_SE": "Ta bort sparad data för detta spel?",
|
||||
"th_TH": "ลบข้อมูลบันทึกของเกมนี้หรือไม่?",
|
||||
"tr_TR": "Bu oyun için kaydedilen veriyi silmek?",
|
||||
"uk_UA": "Видалити збереження даних для цієї гри?",
|
||||
"zh_CN": "删除此游戏的存档数据?",
|
||||
"zh_TW": "刪除此遊戲的存檔資料?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SaveManagerTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "حفظات {0}",
|
||||
"de_DE": "{0}s Speicherstände",
|
||||
"el_GR": "Αποθηκεύσεις του {0}",
|
||||
"en_US": "{0}'s Saves",
|
||||
"es_ES": "Guardados de {0}",
|
||||
"fr_FR": "Sauvegardes de {0}",
|
||||
"he_IL": "שמירות של {0}",
|
||||
"it_IT": "Salvataggi di {0}",
|
||||
"ja_JP": "{0} のセーブデータ",
|
||||
"ko_KR": "{0} 의 저장",
|
||||
"no_NO": "Lagringer til {0}",
|
||||
"pl_PL": "Zapisy {0}",
|
||||
"pt_BR": "Salvamentos de {0}",
|
||||
"ru_RU": "Сохранения {0}",
|
||||
"sv_SE": "{0}s Sparningar",
|
||||
"th_TH": "ข้อมูลที่บันทึกไว้ของ {0}",
|
||||
"tr_TR": "{0}’nin Kayıtları",
|
||||
"uk_UA": "Збереження {0}",
|
||||
"zh_CN": "{0} 的存档",
|
||||
"zh_TW": "{0} 的存檔"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RecoverLostProfiles",
|
||||
"Translations": {
|
||||
"ar_SA": "الملفات الشخصية المفقودة",
|
||||
"de_DE": "Verlorene Profile",
|
||||
"el_GR": "Χαμένα προφίλ",
|
||||
"en_US": "Lost Profiles",
|
||||
"es_ES": "Perfiles Perdidos",
|
||||
"fr_FR": "Profils Perdus",
|
||||
"he_IL": "פרופילים אבודים",
|
||||
"it_IT": "Profili persi",
|
||||
"ja_JP": "失われたプロフィール",
|
||||
"ko_KR": "분실된 프로필",
|
||||
"no_NO": "Tapte profiler",
|
||||
"pl_PL": "Utracone profile",
|
||||
"pt_BR": "Perfis perdidos",
|
||||
"ru_RU": "Потерянные учёные записи",
|
||||
"sv_SE": "Förlorade profiler",
|
||||
"th_TH": "โปรไฟล์ที่สูญหาย",
|
||||
"tr_TR": "Kayıp profiller",
|
||||
"uk_UA": "Втрачені профілі",
|
||||
"zh_CN": "丢失的个人资料",
|
||||
"zh_TW": "遺失的個人資料"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RecoverLostProfiles_ToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "يستعيد الملفات الشخصية التي لم تُحذف يدويًا والتي تحتوي على حفظات.",
|
||||
"de_DE": "Stellt nicht manuell gelöschte Profile mit Speicherständen wieder.",
|
||||
"el_GR": "Ανακτά προφίλ που δεν διαγράφηκαν χειροκίνητα και έχουν αποθηκεύσεις.",
|
||||
"en_US": "Recovers non-manually-deleted profiles that have saves.",
|
||||
"es_ES": "Recupera perfiles no eliminados manualmente que tienen guardados.",
|
||||
"fr_FR": "Récupère les profils non supprimés manuellement ayant des sauvegardes.",
|
||||
"he_IL": "שחזור פרופילים שלא נמחקו ידנית ויש להם שמירות.",
|
||||
"it_IT": "Recupera profili non eliminati manualmente che hanno salvataggi.",
|
||||
"ja_JP": "手動で削除されていない、保存されたプロフィールを回復します。",
|
||||
"ko_KR": "수동으로 삭제되지 않은 저장된 프로필을 복구합니다.",
|
||||
"no_NO": "Gjenoppretter profiler som ikke er manuelt slettet og som har lagringer.",
|
||||
"pl_PL": "Odzyskuje profile, które nie zostały usunięte ręcznie, a które mają zapisy.",
|
||||
"pt_BR": "Recupera perfis não deletados manualmente que possuem saves.",
|
||||
"ru_RU": "Восстанавливает учётные записи, не удалённые вручную и имеющие сохранения.",
|
||||
"sv_SE": "Återställer profiler som inte har raderats manuellt och har sparade data.",
|
||||
"th_TH": "กู้คืนโปรไฟล์ที่ไม่ได้ลบด้วยตนเองและมีการบันทึก",
|
||||
"tr_TR": "Manuel olarak silinmemiş ve kayıtlara sahip profilleri kurtarır.",
|
||||
"uk_UA": "Відновлює учётні записи, які не були видалені вручну і мають збереження.",
|
||||
"zh_CN": "恢复未手动删除且有存档的个人资料。",
|
||||
"zh_TW": "恢復未手動刪除且有存檔的個人資料。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RecoverProfile",
|
||||
"Translations": {
|
||||
"ar_SA": "استعادة",
|
||||
"de_DE": "Wiederherstellen",
|
||||
"el_GR": "Ανάκτηση",
|
||||
"en_US": "Recover",
|
||||
"es_ES": "Recuperar",
|
||||
"fr_FR": "Récupérer",
|
||||
"he_IL": "שחזר",
|
||||
"it_IT": "Recupera",
|
||||
"ja_JP": "復旧",
|
||||
"ko_KR": "복구",
|
||||
"no_NO": "Gjenopprett",
|
||||
"pl_PL": "Odzyskaj",
|
||||
"pt_BR": "Recuperar",
|
||||
"ru_RU": "Восстановить",
|
||||
"sv_SE": "Återskapa",
|
||||
"th_TH": "กู้คืน",
|
||||
"tr_TR": "Kurtar",
|
||||
"uk_UA": "Відновити",
|
||||
"zh_CN": "恢复",
|
||||
"zh_TW": "復原"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RecoverProfile_EmptyList",
|
||||
"Translations": {
|
||||
"ar_SA": "لا توجد ملفات شخصية لاستردادها",
|
||||
"de_DE": "Keine Profile zum Wiederherstellen",
|
||||
"el_GR": "Δεν υπάρχουν προφίλ για ανάκτηση",
|
||||
"en_US": "No Profiles To Recover",
|
||||
"es_ES": "No hay perfiles a recuperar",
|
||||
"fr_FR": "Aucun profil à restaurer",
|
||||
"he_IL": "אין פרופילים לשחזור",
|
||||
"it_IT": "Nessun profilo da recuperare",
|
||||
"ja_JP": "復元するプロファイルはありません",
|
||||
"ko_KR": "복구할 프로필 없음",
|
||||
"no_NO": "Ingen profiler å gjenopprette",
|
||||
"pl_PL": "Brak profili do odzyskania",
|
||||
"pt_BR": "Nenhum perfil para recuperar",
|
||||
"ru_RU": "Нет учётных записей для восстановления",
|
||||
"sv_SE": "Inga profiler att återskapa",
|
||||
"th_TH": "ไม่มีโปรไฟล์ที่สามารถกู้คืนได้",
|
||||
"tr_TR": "Kurtarılacak profil bulunamadı",
|
||||
"uk_UA": "Немає профілів для відновлення",
|
||||
"zh_CN": "没有可以恢复的用户数据",
|
||||
"zh_TW": "無設定檔可復原"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_SortByName",
|
||||
"Translations": {
|
||||
"ar_SA": "الاسم",
|
||||
"de_DE": "",
|
||||
"el_GR": "Όνομα",
|
||||
"en_US": "Name",
|
||||
"es_ES": "Nombre",
|
||||
"fr_FR": "Nom",
|
||||
"he_IL": "שם",
|
||||
"it_IT": "Nome",
|
||||
"ja_JP": "名称",
|
||||
"ko_KR": "이름",
|
||||
"no_NO": "Navn",
|
||||
"pl_PL": "Nazwa",
|
||||
"pt_BR": "Nome",
|
||||
"ru_RU": "Название",
|
||||
"sv_SE": "Namn",
|
||||
"th_TH": "ชื่อ",
|
||||
"tr_TR": "İsim",
|
||||
"uk_UA": "Назва",
|
||||
"zh_CN": "名称",
|
||||
"zh_TW": "名稱"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_SortBySize",
|
||||
"Translations": {
|
||||
"ar_SA": "الحجم",
|
||||
"de_DE": "Größe",
|
||||
"el_GR": "Μέγεθος",
|
||||
"en_US": "Size",
|
||||
"es_ES": "Tamaño",
|
||||
"fr_FR": "Taille",
|
||||
"he_IL": "גודל",
|
||||
"it_IT": "Dimensione",
|
||||
"ja_JP": "サイズ",
|
||||
"ko_KR": "크기",
|
||||
"no_NO": "Størrelse",
|
||||
"pl_PL": "Rozmiar",
|
||||
"pt_BR": "Tamanho",
|
||||
"ru_RU": "Размер",
|
||||
"sv_SE": "Storlek",
|
||||
"th_TH": "ขนาด",
|
||||
"tr_TR": "Boyut",
|
||||
"uk_UA": "Розмір",
|
||||
"zh_CN": "大小",
|
||||
"zh_TW": "大小"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_SortOrderAscending",
|
||||
"Translations": {
|
||||
"ar_SA": "تصاعدي",
|
||||
"de_DE": "Aufsteigend",
|
||||
"el_GR": "Αύξουσα",
|
||||
"en_US": "Ascending",
|
||||
"es_ES": "Ascendente",
|
||||
"fr_FR": "Croissant",
|
||||
"he_IL": "סדר עולה",
|
||||
"it_IT": "Crescente",
|
||||
"ja_JP": "昇順",
|
||||
"ko_KR": "오름차순",
|
||||
"no_NO": "Stigende",
|
||||
"pl_PL": "Rosnąco",
|
||||
"pt_BR": "Ascendente",
|
||||
"ru_RU": "По Возрастанию",
|
||||
"sv_SE": "Stigande",
|
||||
"th_TH": "จากน้อยไปมาก",
|
||||
"tr_TR": "Artan",
|
||||
"uk_UA": "За зростанням",
|
||||
"zh_CN": "升序",
|
||||
"zh_TW": "從小到大"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_SortOrderDescending",
|
||||
"Translations": {
|
||||
"ar_SA": "تنازلي",
|
||||
"de_DE": "Absteigend",
|
||||
"el_GR": "Φθίνουσα",
|
||||
"en_US": "Descending",
|
||||
"es_ES": "Descendente",
|
||||
"fr_FR": "Décroissant",
|
||||
"he_IL": "סדר יורד",
|
||||
"it_IT": "Decrescente",
|
||||
"ja_JP": "降順",
|
||||
"ko_KR": "내림차순",
|
||||
"no_NO": "Synkende",
|
||||
"pl_PL": "Malejąco",
|
||||
"pt_BR": "Descendente",
|
||||
"ru_RU": "По Убыванию",
|
||||
"sv_SE": "Fallande",
|
||||
"th_TH": "จากมากไปน้อย",
|
||||
"tr_TR": "Azalan",
|
||||
"uk_UA": "За спаданням",
|
||||
"zh_CN": "降序",
|
||||
"zh_TW": "從大到小"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_Search",
|
||||
"Translations": {
|
||||
"ar_SA": "بحث",
|
||||
"de_DE": "Suche",
|
||||
"el_GR": "Αναζήτηση",
|
||||
"en_US": "Search",
|
||||
"es_ES": "Buscar",
|
||||
"fr_FR": "Rechercher",
|
||||
"he_IL": "חפש",
|
||||
"it_IT": "Cerca",
|
||||
"ja_JP": "検索",
|
||||
"ko_KR": "찾기",
|
||||
"no_NO": "Søk",
|
||||
"pl_PL": "Wyszukaj",
|
||||
"pt_BR": "Buscar",
|
||||
"ru_RU": "Поиск",
|
||||
"sv_SE": "Sök",
|
||||
"th_TH": "ค้นหา",
|
||||
"tr_TR": "Ara",
|
||||
"uk_UA": "Пошук",
|
||||
"zh_CN": "搜索",
|
||||
"zh_TW": "搜尋"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "IrreversibleActionNote",
|
||||
"Translations": {
|
||||
"ar_SA": "هذا الإجراء لا يمكن التراجع عنه.",
|
||||
"de_DE": "Diese Option kann nicht rückgängig gemacht werden.",
|
||||
"el_GR": "Αυτή η ενέργεια είναι μη αναστρέψιμη.",
|
||||
"en_US": "This action is not reversible.",
|
||||
"es_ES": "Esta acción no es reversible.",
|
||||
"fr_FR": "Cette action n'est pas réversible.",
|
||||
"he_IL": "הפעולה הזו בלתי הפיכה.",
|
||||
"it_IT": "Questa azione non è reversibile.",
|
||||
"ja_JP": "この操作は元に戻せません.",
|
||||
"ko_KR": "이 작업은 되돌릴 수 없습니다.",
|
||||
"no_NO": "Denne handlingen er ikke reverserbar.",
|
||||
"pl_PL": "Ta czynność nie jest odwracalna.",
|
||||
"pt_BR": "Esta ação não é reversível.",
|
||||
"ru_RU": "Данное действие является необратимым.",
|
||||
"sv_SE": "Denna åtgärd går inte att ångra.",
|
||||
"th_TH": "การดำเนินการนี้ไม่สามารถย้อนกลับได้",
|
||||
"tr_TR": "Bu eylem geri alınamaz.",
|
||||
"uk_UA": "Цю дію не можна скасувати.",
|
||||
"zh_CN": "删除后不可恢复。",
|
||||
"zh_TW": "此動作將無法復原。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ButtonClose",
|
||||
"Translations": {
|
||||
"ar_SA": "إغلاق",
|
||||
"de_DE": "Schließen",
|
||||
"el_GR": "Κλείσιμο",
|
||||
"en_US": "Close",
|
||||
"es_ES": "Cerrar",
|
||||
"fr_FR": "Fermer",
|
||||
"he_IL": "סגירה",
|
||||
"it_IT": "Chiudi",
|
||||
"ja_JP": "閉じる",
|
||||
"ko_KR": "닫기",
|
||||
"no_NO": "Lukk",
|
||||
"pl_PL": "Zamknij",
|
||||
"pt_BR": "Fechar",
|
||||
"ru_RU": "Закрыть",
|
||||
"sv_SE": "Stäng",
|
||||
"th_TH": "ปิด",
|
||||
"tr_TR": "Kapat",
|
||||
"uk_UA": "Закрити",
|
||||
"zh_CN": "关闭",
|
||||
"zh_TW": "關閉"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "NameLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "الاسم:",
|
||||
"de_DE": null,
|
||||
"el_GR": "Όνομα:",
|
||||
"en_US": "Name:",
|
||||
"es_ES": "Nombre:",
|
||||
"fr_FR": "Nom :",
|
||||
"he_IL": "שם:",
|
||||
"it_IT": "Nome:",
|
||||
"ja_JP": "名称:",
|
||||
"ko_KR": "이름 :",
|
||||
"no_NO": "Navn:",
|
||||
"pl_PL": "Nazwa:",
|
||||
"pt_BR": "Nome:",
|
||||
"ru_RU": "Имя:",
|
||||
"sv_SE": "Namn:",
|
||||
"th_TH": "ชื่อ:",
|
||||
"tr_TR": "İsim:",
|
||||
"uk_UA": "Імʼя",
|
||||
"zh_CN": "名称:",
|
||||
"zh_TW": "名稱:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ProfileNameSelectionWatermark",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر اسم الملف الشخصي",
|
||||
"de_DE": "Wähle einen Profilnamen",
|
||||
"el_GR": "Επιλέξτε όνομα προφίλ",
|
||||
"en_US": "Choose a Profile Name",
|
||||
"es_ES": "Escoge un Nombre de Perfil",
|
||||
"fr_FR": "Choisir un Nom de Profil",
|
||||
"he_IL": "בחרו שם פרופיל",
|
||||
"it_IT": "Scegli un Nome Profilo",
|
||||
"ja_JP": "プロフィール名を選択",
|
||||
"ko_KR": "프로필 이름 선택",
|
||||
"no_NO": "Velg et Profilnavn",
|
||||
"pl_PL": "Wybierz nazwę profilu",
|
||||
"pt_BR": "Escolha um Nome de Perfil",
|
||||
"ru_RU": "Выберите имя профиля",
|
||||
"sv_SE": "Välj ett Profilnamn",
|
||||
"th_TH": "เลือก ชื่อโปรไฟล์",
|
||||
"tr_TR": "Profil Adı Seç",
|
||||
"uk_UA": "Оберіть ім'я профілю",
|
||||
"zh_CN": "选择个人资料名称",
|
||||
"zh_TW": "選擇個人資料名稱"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "UserIdLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "معرف المستخدم:",
|
||||
"de_DE": "Benutzer-ID:",
|
||||
"el_GR": "Ταυτότητα Χρήστη:",
|
||||
"en_US": "User ID:",
|
||||
"es_ES": "ID de Usuario:",
|
||||
"fr_FR": "Identifiant Utilisateur :",
|
||||
"he_IL": "מזהה משתמש:",
|
||||
"it_IT": "ID utente:",
|
||||
"ja_JP": "ユーザID:",
|
||||
"ko_KR": "사용자 ID :",
|
||||
"no_NO": "Bruker ID:",
|
||||
"pl_PL": "ID Użytkownika:",
|
||||
"pt_BR": "ID de Usuário:",
|
||||
"ru_RU": "ID пользователя:",
|
||||
"sv_SE": "Användar-id:",
|
||||
"th_TH": "รหัสผู้ใช้:",
|
||||
"tr_TR": "Kullanıcı ID:",
|
||||
"uk_UA": "ID користувача:",
|
||||
"zh_CN": "用户 ID:",
|
||||
"zh_TW": "使用者 ID:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ProfileImage_Import",
|
||||
"Translations": {
|
||||
"ar_SA": "استيراد الصورة",
|
||||
"de_DE": "Bild importieren",
|
||||
"el_GR": "Εισαγωγή Εικόνας",
|
||||
"en_US": "Import Image",
|
||||
"es_ES": "Importar Imagen",
|
||||
"fr_FR": "Importer une image",
|
||||
"he_IL": "ייבוא תמונה",
|
||||
"it_IT": "Importa immagine",
|
||||
"ja_JP": "画像をインポート",
|
||||
"ko_KR": "이미지 가져오기",
|
||||
"no_NO": "Importer bilde",
|
||||
"pl_PL": "Importuj obraz",
|
||||
"pt_BR": "Importar Imagem",
|
||||
"ru_RU": "Импорт изображения",
|
||||
"sv_SE": "Importera bild",
|
||||
"th_TH": "นำเข้าภาพ",
|
||||
"tr_TR": "Resim İçeri Aktar",
|
||||
"uk_UA": "Імпорт зображення",
|
||||
"zh_CN": "导入图像",
|
||||
"zh_TW": "匯入圖像"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ProfileImage_SelectAvatar",
|
||||
"Translations": {
|
||||
"ar_SA": "حدد صورة الأفاتار من البرنامج الثابت",
|
||||
"de_DE": "Firmware-Avatar auswählen",
|
||||
"el_GR": "Επιλέξτε Avatar από Firmware",
|
||||
"en_US": "Select Firmware Avatar",
|
||||
"es_ES": "Seleccionar Avatar del Firmware",
|
||||
"fr_FR": "Choisir un Avatar du Firmware",
|
||||
"he_IL": "בחרו אוואטר קושחה",
|
||||
"it_IT": "Seleziona avatar dal firmware",
|
||||
"ja_JP": "ファームウェア内のアバターを選択",
|
||||
"ko_KR": "펌웨어 아바타 선택",
|
||||
"no_NO": "Velg firmware-avatar",
|
||||
"pl_PL": "Wybierz avatar z oprogramowania",
|
||||
"pt_BR": "Selecionar Avatar do Firmware",
|
||||
"ru_RU": "Выбрать аватар прошивки",
|
||||
"sv_SE": "Välj avatar från firmware",
|
||||
"th_TH": "เลือกอวาต้าจากระบบ",
|
||||
"tr_TR": "Yazılım Avatarı Seç",
|
||||
"uk_UA": "Виберіть аватар прошивки",
|
||||
"zh_CN": "选择固件头像",
|
||||
"zh_TW": "選取韌體大頭貼"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SupportedImageFormatDialogTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر إما JPG أو JPEG أو PNG أو BMP",
|
||||
"de_DE": "Wählen Sie entweder ein JPG, JPEG, PNG oder BMP",
|
||||
"el_GR": "Επιλέξτε είτε JPG, JPEG, PNG ή BMP",
|
||||
"en_US": "Choose either a JPG, JPEG, PNG, or BMP",
|
||||
"es_ES": "Elige ya sea JPG, JPEG, PNG o BMP",
|
||||
"fr_FR": "Choisissez soit un JPG, JPEG, PNG ou BMP",
|
||||
"he_IL": "בחר את JPG, JPEG, PNG או BMP",
|
||||
"it_IT": "Scegli tra JPG, JPEG, PNG o BMP",
|
||||
"ja_JP": "JPG、JPEG、PNG、またはBMPのいずれかを選択してください",
|
||||
"ko_KR": "JPG, JPEG, PNG 또는 BMP 중에서 선택하세요",
|
||||
"no_NO": "Velg enten et JPG, JPEG, PNG eller BMP",
|
||||
"pl_PL": "Wybierz JPG, JPEG, PNG lub BMP",
|
||||
"pt_BR": "Escolha JPG, JPEG, PNG ou BMP",
|
||||
"ru_RU": "Выберите либо JPG, JPEG, PNG, или BMP",
|
||||
"sv_SE": "Välj antingen ett JPG, JPEG, PNG eller BMP",
|
||||
"th_TH": "เลือก JPG, JPEG, PNG หรือ BMP",
|
||||
"tr_TR": "JPG, JPEG, PNG veya BMP seçin",
|
||||
"uk_UA": "Виберіть або JPG, JPEG, PNG, або BMP",
|
||||
"zh_CN": "选择 JPG、JPEG、PNG 或 BMP",
|
||||
"zh_TW": "選擇 JPG、JPEG、PNG 或 BMP"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SelectAvatarTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "تحديد أفاتار البرنامج الثابت",
|
||||
"de_DE": "Firmware-Avatar auswählen",
|
||||
"el_GR": "Επιλογή Avatar Firmware",
|
||||
"en_US": "Select Firmware Avatar",
|
||||
"es_ES": "Seleccionar Avatar del Firmware",
|
||||
"fr_FR": "Sélection d’un Avatar du Firmware",
|
||||
"he_IL": "בחירת אוואטר קושחה",
|
||||
"it_IT": "Selezione Avatar Firmware",
|
||||
"ja_JP": "ファームウェアアバター選択",
|
||||
"ko_KR": "펌웨어 아바타 선택",
|
||||
"no_NO": "Velg firmware-avatar",
|
||||
"pl_PL": "Wybór awatara oprogramowania",
|
||||
"pt_BR": "Selecionar Avatar do Firmware",
|
||||
"ru_RU": "Выбор аватара прошивки",
|
||||
"sv_SE": "Välj firmware-avatar",
|
||||
"th_TH": "การเลือกอวตารเฟิร์มแวร์",
|
||||
"tr_TR": "Firmware Avatar Seçimi",
|
||||
"uk_UA": "Вибір аватара прошивки",
|
||||
"zh_CN": "选择固件头像",
|
||||
"zh_TW": "選取韌體頭像"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ButtonChooseAvatar",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر الأفاتار",
|
||||
"de_DE": "Wähle Avatar",
|
||||
"el_GR": "Επιλέξτε Avatar",
|
||||
"en_US": "Choose Avatar",
|
||||
"es_ES": "Elegir Avatar",
|
||||
"fr_FR": "Choisir un Avatar",
|
||||
"he_IL": "בחרו אוואטר",
|
||||
"it_IT": "Scegli Avatar",
|
||||
"ja_JP": "アバターを選択",
|
||||
"ko_KR": "아바타 선택",
|
||||
"no_NO": "Velg avatar",
|
||||
"pl_PL": "Wybierz awatar",
|
||||
"pt_BR": "Escolher Avatar",
|
||||
"ru_RU": "Выбрать аватар",
|
||||
"sv_SE": "Välj avatar",
|
||||
"th_TH": "เลือกอวาต้าของคุณ",
|
||||
"tr_TR": "Avatar Seç",
|
||||
"uk_UA": "Вибрати аватар",
|
||||
"zh_CN": "选择头像",
|
||||
"zh_TW": "選擇大頭貼"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileUnsavedChangesMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "لقد قمت بإجراء تغييرات غير محفوظة على هذا الملف الشخصي.",
|
||||
"de_DE": "Sie haben nicht gespeicherte Änderungen an diesem Profil.",
|
||||
"el_GR": "Έχετε μη αποθηκευμένες αλλαγές σε αυτό το προφίλ.",
|
||||
"en_US": "You have unsaved changes to this profile.",
|
||||
"es_ES": "Tienes cambios no guardados en este perfil.",
|
||||
"fr_FR": "Vous avez des modifications non enregistrées sur ce profil.",
|
||||
"he_IL": "ביצעת שינויים לא שמורים בפרופיל זה.",
|
||||
"it_IT": "Hai modifiche non salvate su questo profilo.",
|
||||
"ja_JP": "このプロファイルには保存されていない変更があります.",
|
||||
"ko_KR": "이 프로필에는 저장되지 않은 변경 사항이 있습니다.",
|
||||
"no_NO": "Du har usparende endringer på denne profilen.",
|
||||
"pl_PL": "Masz niezapisane zmiany w tym profilu.",
|
||||
"pt_BR": "Você tem alterações não salvas neste perfil.",
|
||||
"ru_RU": "У вас есть несохраненные изменения в этом профиле.",
|
||||
"sv_SE": "Du har osparade ändringar i den här profilen.",
|
||||
"th_TH": "คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึกในโปรไฟล์นี้",
|
||||
"tr_TR": "Bu profilde kaydedilmemiş değişiklikleriniz var.",
|
||||
"uk_UA": "У вас є незбережені зміни в цьому профілі.",
|
||||
"zh_CN": "您对该账户有未保存的更改。",
|
||||
"zh_TW": "您對該使用者設定檔有未儲存的變更。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileUnsavedChangesSubMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "هل تريد تجاهل التغييرات؟",
|
||||
"de_DE": "Verwerfen Sie die Änderungen?",
|
||||
"el_GR": "Θέλετε να απορρίψετε τις αλλαγές?",
|
||||
"en_US": "Discard changes?",
|
||||
"es_ES": "¿Descartar los cambios?",
|
||||
"fr_FR": "Annuler les modifications ?",
|
||||
"he_IL": "האם ברצונך להתעלם מהשינויים?",
|
||||
"it_IT": "Scartare le modifiche?",
|
||||
"ja_JP": "変更を破棄しますか?",
|
||||
"ko_KR": "변경 사항을 취소하시겠습니까?",
|
||||
"no_NO": "Vil du forkaste endringene?",
|
||||
"pl_PL": "Czy chcesz odrzucić zmiany?",
|
||||
"pt_BR": "Deseja descartar as alterações?",
|
||||
"ru_RU": "Отменить изменения?",
|
||||
"sv_SE": "Vill du förkasta ändringarna?",
|
||||
"th_TH": "คุณต้องการทิ้งการเปลี่ยนแปลงหรือไม่?",
|
||||
"tr_TR": "Değişiklikleri iptal et?",
|
||||
"uk_UA": "Бажаєте скасувати зміни?",
|
||||
"zh_CN": "确定要放弃更改吗?",
|
||||
"zh_TW": "您確定要放棄變更嗎?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileUnsavedChangesTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "تحذير - التغييرات غير محفوظة",
|
||||
"de_DE": "WARNUNG - Nicht gespeicherte Änderungen",
|
||||
"el_GR": "ΠΡΟΣΟΧΗ - Μην Αποθηκευμένες Αλλαγές.",
|
||||
"en_US": "WARNING - Unsaved Changes",
|
||||
"es_ES": "ADVERTENCIA - Cambios Sin Guardar",
|
||||
"fr_FR": "AVERTISSEMENT - Modifications Non Enregistrées",
|
||||
"he_IL": "אזהרה - שינויים לא שמורים",
|
||||
"it_IT": "ATTENZIONE - Modifiche non salvate",
|
||||
"ja_JP": "警告 - 保存されていない変更",
|
||||
"ko_KR": "경고 - 저장되지 않은 변경 사항",
|
||||
"no_NO": "ADVARSEL - Ulagrede endringer",
|
||||
"pl_PL": "UWAGA - Niezapisane zmiany",
|
||||
"pt_BR": "ALERTA - Alterações não salvas",
|
||||
"ru_RU": "ВНИМАНИЕ - Несохраненные изменения",
|
||||
"sv_SE": "VARNING - Ej sparade ändringar",
|
||||
"th_TH": "คำเตือน - มีการเปลี่ยนแปลงที่ไม่ได้บันทึก",
|
||||
"tr_TR": "UYARI - Kaydedilmemiş Değişiklikler",
|
||||
"uk_UA": "УВАГА — Незбережені зміни",
|
||||
"zh_CN": "警告 - 有未保存的更改",
|
||||
"zh_TW": "警告 - 未儲存的變更"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileDeletionConfirmMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "هل حذف الملف الشخصي المحدد؟",
|
||||
"de_DE": "Löschen Sie das ausgewählte Profil?",
|
||||
"el_GR": "Διαγραφή του επιλεγμένου προφίλ;",
|
||||
"en_US": "Delete the selected profile?",
|
||||
"es_ES": "¿Eliminar el perfil seleccionado?",
|
||||
"fr_FR": "Supprimer le profil sélectionné ?",
|
||||
"he_IL": "האם למחוק את הפרופיל שנבחר?",
|
||||
"it_IT": "Eliminare il profilo selezionato?",
|
||||
"ja_JP": "選択されたプロファイルを削除しますか?",
|
||||
"ko_KR": "선택한 프로필을 삭제하시겠습니까?",
|
||||
"no_NO": "Slette den valgte profilen?",
|
||||
"pl_PL": "Usunąć wybrany profil?",
|
||||
"pt_BR": "Excluir o perfil selecionado?",
|
||||
"ru_RU": "Удалить выбранный профиль?",
|
||||
"sv_SE": "Ta bort den valda profilen?",
|
||||
"th_TH": "ลบโปรไฟล์ที่เลือก?",
|
||||
"tr_TR": "Seçilen profili silmek?",
|
||||
"uk_UA": "Видалити вибраний профіль?",
|
||||
"zh_CN": "删除所选账户?",
|
||||
"zh_TW": "刪除所選設定檔?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileDeletionWarningMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "لن تكون هناك ملفات الشخصية أخرى لفتحها إذا تم حذف الملف الشخصي المحدد.",
|
||||
"de_DE": "Es können keine anderen Profile geöffnet werden, wenn das ausgewählte Profil gelöscht wird.",
|
||||
"el_GR": "Δεν θα υπάρχουν άλλα προφίλ εάν διαγραφεί το επιλεγμένο.",
|
||||
"en_US": "There would be no other profiles to be opened if selected profile is deleted.",
|
||||
"es_ES": "Si eliminas el perfil seleccionado no quedará ningún otro perfil.",
|
||||
"fr_FR": "Il n'y aurait aucun autre profil à ouvrir si le profil sélectionné est supprimé.",
|
||||
"he_IL": "לא יהיו פרופילים אחרים שייפתחו אם הפרופיל שנבחר יימחק.",
|
||||
"it_IT": "Non ci sarebbero altri profili da aprire se il profilo selezionato venisse cancellato.",
|
||||
"ja_JP": "選択されたプロファイルを削除すると,プロファイルがひとつも存在しなくなります.",
|
||||
"ko_KR": "선택한 프로필을 삭제하면 다른 프로필을 열 수 없음.",
|
||||
"no_NO": "Det vil ikke være noen profiler å åpnes hvis valgt profil blir slettet.",
|
||||
"pl_PL": "Nie będzie innych profili do otwarcia, jeśli wybrany profil zostanie usunięty.",
|
||||
"pt_BR": "Não haveria nenhum perfil selecionado se o perfil atual fosse deletado.",
|
||||
"ru_RU": "Если выбранный профиль будет удален, другие профили не будут открываться.",
|
||||
"sv_SE": "Det skulle inte finnas några andra profiler att öppnas om angiven profil tas bort.",
|
||||
"th_TH": "จะไม่มีโปรไฟล์อื่นให้เปิดหากโปรไฟล์ที่เลือกถูกลบ",
|
||||
"tr_TR": "Seçilen profil silinirse kullanılabilen başka profil kalmayacak.",
|
||||
"uk_UA": "Якщо вибраний профіль буде видалено, інші профілі не відкриватимуться.",
|
||||
"zh_CN": "删除后将没有可用的账户。",
|
||||
"zh_TW": "如果刪除選取的設定檔,將無法開啟其他設定檔。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ButtonDelete",
|
||||
"Translations": {
|
||||
"ar_SA": "حذف",
|
||||
"de_DE": "Löschen",
|
||||
"el_GR": "Διαγράφω",
|
||||
"en_US": "Delete",
|
||||
"es_ES": "Eliminar",
|
||||
"fr_FR": "Supprimer",
|
||||
"he_IL": "מחיקה",
|
||||
"it_IT": "Elimina",
|
||||
"ja_JP": "削除",
|
||||
"ko_KR": "삭제",
|
||||
"no_NO": "Slett",
|
||||
"pl_PL": "Usuń",
|
||||
"pt_BR": "Apagar",
|
||||
"ru_RU": "Удалить",
|
||||
"sv_SE": "Ta bort",
|
||||
"th_TH": "ลบ",
|
||||
"tr_TR": "Sil",
|
||||
"uk_UA": "Видалити",
|
||||
"zh_CN": "删除",
|
||||
"zh_TW": "刪除"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ButtonSave",
|
||||
"Translations": {
|
||||
"ar_SA": "حفظ",
|
||||
"de_DE": "Speichern",
|
||||
"el_GR": "Αποθήκευση",
|
||||
"en_US": "Save",
|
||||
"es_ES": "Guardar",
|
||||
"fr_FR": "Enregistrer",
|
||||
"he_IL": "שמור",
|
||||
"it_IT": "Salva",
|
||||
"ja_JP": "セーブ",
|
||||
"ko_KR": "저장",
|
||||
"no_NO": "Lagre",
|
||||
"pl_PL": "Zapisz",
|
||||
"pt_BR": "Salvar",
|
||||
"ru_RU": "Сохранить",
|
||||
"sv_SE": "Spara",
|
||||
"th_TH": "บันทึก",
|
||||
"tr_TR": "Kaydet",
|
||||
"uk_UA": "Зберегти",
|
||||
"zh_CN": "保存",
|
||||
"zh_TW": "儲存"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "UserEditorTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "جارٍ تعديل {0}",
|
||||
"de_DE": "{0} wird bearbeitet",
|
||||
"el_GR": "Επεξεργασία {0}",
|
||||
"en_US": "Editing {0}",
|
||||
"es_ES": "Editando {0}",
|
||||
"fr_FR": "Modification de {0}",
|
||||
"he_IL": "עריכת {0}",
|
||||
"it_IT": "Modifica di {0}",
|
||||
"ja_JP": "{0} を編集中",
|
||||
"ko_KR": "{0} 편집 중",
|
||||
"no_NO": "Redigerer {0}",
|
||||
"pl_PL": "Edycja {0}",
|
||||
"pt_BR": "Editando {0}",
|
||||
"ru_RU": "Редактирование {0}",
|
||||
"sv_SE": "Redigerar {0}",
|
||||
"th_TH": "กำลังกำลังแก้ไข {0}",
|
||||
"tr_TR": "{0} düzenleniyor",
|
||||
"uk_UA": "Редагування {0}",
|
||||
"zh_CN": "正在编辑 {0}",
|
||||
"zh_TW": "正在編輯 {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "UserEditorTitleNewUser",
|
||||
"Translations": {
|
||||
"ar_SA": "مستخدم جديد",
|
||||
"de_DE": "Neuer Nutzer",
|
||||
"el_GR": "Νέος Χρήστης",
|
||||
"en_US": "New User",
|
||||
"es_ES": "Nuevo Usuario",
|
||||
"fr_FR": "Nouvel Utilisateur",
|
||||
"he_IL": "משתמש חדש",
|
||||
"it_IT": "Nuovo utente",
|
||||
"ja_JP": "新しいユーザー",
|
||||
"ko_KR": "새 사용자",
|
||||
"no_NO": "Ny bruker",
|
||||
"pl_PL": "Nowy użytkownik",
|
||||
"pt_BR": "Novo usuário",
|
||||
"ru_RU": "Новый пользователь",
|
||||
"sv_SE": "Ny användare",
|
||||
"th_TH": "ผู้ใช้ใหม่",
|
||||
"tr_TR": "Yeni kullanıcı",
|
||||
"uk_UA": "Новий користувач",
|
||||
"zh_CN": "新用户",
|
||||
"zh_TW": "新使用者"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "EmptyNameError",
|
||||
"Translations": {
|
||||
"ar_SA": "الاسم مطلوب",
|
||||
"de_DE": "Name ist erforderlich",
|
||||
"el_GR": "Απαιτείται όνομα",
|
||||
"en_US": "Name is required",
|
||||
"es_ES": "El nombre es obligatorio",
|
||||
"fr_FR": "Le nom est requis",
|
||||
"he_IL": "נדרש שם",
|
||||
"it_IT": "Il nome è obbligatorio",
|
||||
"ja_JP": "名称が必要です",
|
||||
"ko_KR": "이름 필수 입력",
|
||||
"no_NO": "Navn er påkrevd",
|
||||
"pl_PL": "Nazwa jest wymagana",
|
||||
"pt_BR": "Nome é obrigatório",
|
||||
"ru_RU": "Необходимо ввести имя",
|
||||
"sv_SE": "Namn krävs",
|
||||
"th_TH": "จำเป็นต้องระบุชื่อ",
|
||||
"tr_TR": "İsim gerekli",
|
||||
"uk_UA": "Імʼя обовʼязкове",
|
||||
"zh_CN": "必须输入名称",
|
||||
"zh_TW": "名稱為必填"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -22,5 +22,5 @@ chmod +x AppDir/AppRun AppDir/usr/bin/Ryujinx*
|
||||
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
appimagetool --appimage-extract-and-run -n --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 21 \
|
||||
AppDir "$OUTDIR"/Ryujinx.AppImage
|
||||
appimagetool -n --comp zstd --mksquashfs-opt -Xcompression-level --mksquashfs-opt 21 \
|
||||
AppDir "$OUTDIR"/Ryujinx.AppImage
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$#" -lt 8 ]; then
|
||||
echo "usage <BASE_DIR> <TEMP_DIRECTORY> <OUTPUT_DIRECTORY> <ENTITLEMENTS_FILE_PATH> <VERSION> <SOURCE_REVISION_ID> <CONFIGURATION>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$1"
|
||||
mkdir -p "$2"
|
||||
mkdir -p "$3"
|
||||
|
||||
BASE_DIR=$(readlink -f "$1")
|
||||
TEMP_DIRECTORY=$(readlink -f "$2")
|
||||
OUTPUT_DIRECTORY=$(readlink -f "$3")
|
||||
ENTITLEMENTS_FILE_PATH=$(readlink -f "$4")
|
||||
VERSION=$5
|
||||
SOURCE_REVISION_ID=$6
|
||||
CONFIGURATION=$7
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
echo "Clearing xattr on all dot undercsore files"
|
||||
find "$BASE_DIR" -type f -name "._*" -exec sh -c '
|
||||
for f; do
|
||||
dir=$(dirname "$f")
|
||||
base=$(basename "$f")
|
||||
orig="$dir/${base#._}"
|
||||
[ -f "$orig" ] && xattr -c "$orig" || true
|
||||
done
|
||||
' sh {} +
|
||||
fi
|
||||
|
||||
RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar
|
||||
|
||||
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"
|
||||
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 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"
|
||||
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
|
||||
|
||||
# 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"
|
||||
|
||||
# Now sign it
|
||||
if ! [ -x "$(command -v codesign)" ];
|
||||
then
|
||||
if ! [ -x "$(command -v rcodesign)" ];
|
||||
then
|
||||
echo "Cannot find rcodesign on your system, please install rcodesign."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 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"
|
||||
else
|
||||
echo "Using codesign for ad-hoc signing"
|
||||
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$UNIVERSAL_APP_BUNDLE"
|
||||
fi
|
||||
|
||||
echo "Creating archive"
|
||||
pushd "$OUTPUT_DIRECTORY"
|
||||
tar --exclude "Ryujinx.app/Contents/MacOS/Ryujinx" -cvf "$RELEASE_TAR_FILE_NAME" Ryujinx.app 1> /dev/null
|
||||
python3 "$BASE_DIR/distribution/misc/add_tar_exec.py" "$RELEASE_TAR_FILE_NAME" "Ryujinx.app/Contents/MacOS/Ryujinx" "Ryujinx.app/Contents/MacOS/Ryujinx"
|
||||
gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
|
||||
rm "$RELEASE_TAR_FILE_NAME"
|
||||
|
||||
popd
|
||||
|
||||
echo "Done"
|
||||
@@ -5,7 +5,8 @@
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<!-- Only needed when using pre-release versions of Ryujinx.LibHac. -->
|
||||
<add key="LibHacAlpha" value="https://git.ryujinx.app/api/packages/projects/nuget/index.json" />
|
||||
<add key="LibHacAlpha" value="https://git.ryujinx.app/api/v4/projects/17/packages/nuget/index.json" />
|
||||
<add key="Ryujinx.UpdateClient" value="https://git.ryujinx.app/api/v4/projects/71/packages/nuget/index.json" />
|
||||
</packageSources>
|
||||
<packageSourceMapping>
|
||||
<!-- key value for <packageSource> should match key values from <packageSources> element -->
|
||||
@@ -13,6 +14,10 @@
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
<packageSource key="Ryujinx.UpdateClient">
|
||||
<package pattern="Ryujinx.UpdateClient" />
|
||||
<package pattern="Ryujinx.Systems.Update.Common" />
|
||||
</packageSource>
|
||||
<packageSource key="LibHacAlpha">
|
||||
<package pattern="Ryujinx.LibHac" />
|
||||
</packageSource>
|
||||
|
||||
@@ -107,12 +107,12 @@ namespace Ryujinx.BuildValidationTasks
|
||||
{
|
||||
locale.Translations[langCode] = string.Empty;
|
||||
Console.WriteLine(
|
||||
$"Language '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
|
||||
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"Language '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
|
||||
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,15 @@ namespace Ryujinx.Common.Helper
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[LibraryImport("user32")]
|
||||
private static partial nint GetForegroundWindow();
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
[LibraryImport("user32")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool SetForegroundWindow(nint hWnd);
|
||||
|
||||
public static bool SetConsoleWindowStateSupported => OperatingSystem.IsWindows();
|
||||
|
||||
public static void SetConsoleWindowState(bool show)
|
||||
@@ -44,6 +53,10 @@ namespace Ryujinx.Common.Helper
|
||||
return;
|
||||
}
|
||||
|
||||
SetForegroundWindow(hWnd);
|
||||
|
||||
hWnd = GetForegroundWindow();
|
||||
|
||||
ShowWindow(hWnd, show ? SW_SHOW : SW_HIDE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ namespace Ryujinx.Common
|
||||
|
||||
public static string GetChangelogUrl(Version currentVersion, Version newVersion) =>
|
||||
IsCanaryBuild
|
||||
? $"https://git.ryujinx.app/projects/Ryubing/compare/Canary-{currentVersion}...Canary-{newVersion}"
|
||||
: $"https://git.ryujinx.app/projects/Ryubing/releases/tag/{newVersion}";
|
||||
? $"https://git.ryujinx.app/ryubing/ryujinx/-/compare/Canary-{currentVersion}...Canary-{newVersion}"
|
||||
: $"https://git.ryujinx.app/ryubing/ryujinx/-/releases/{newVersion}";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" />
|
||||
<PackageReference Include="MsgPack.Cli" />
|
||||
<PackageReference Include="System.Management" />
|
||||
<PackageReference Include="Humanizer" />
|
||||
<PackageReference Include="Gommon" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -8,12 +8,12 @@ namespace Ryujinx.Common
|
||||
|
||||
public const string AmiiboTagsUrl = "https://raw.githubusercontent.com/Ryubing/Nfc/refs/heads/main/tags.json";
|
||||
|
||||
public const string FaqWikiUrl = "https://git.ryujinx.app/projects/Ryubing/wiki/FAQ-%26-Troubleshooting";
|
||||
public const string FaqWikiUrl = "https://git.ryujinx.app/ryubing/ryujinx/-/wikis/FAQ-&-Troubleshooting";
|
||||
|
||||
public const string SetupGuideWikiUrl =
|
||||
"https://git.ryujinx.app/projects/Ryubing/wiki/Setup-%26-Configuration-Guide";
|
||||
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Setup-&-Configuration-Guide";
|
||||
|
||||
public const string MultiplayerWikiUrl =
|
||||
"https://git.ryujinx.app/projects/Ryubing/wiki/Multiplayer-(LDN-Local-Wireless)-Guide";
|
||||
"https://git.ryujinx.app/ryubing/ryujinx/-/wikis/Multiplayer-(LDN-Local-Wireless)-Guide";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,6 @@ namespace Ryujinx.Common
|
||||
"01001b300b9be000", // Diablo III: Eternal Collection
|
||||
"010027400cdc6000", // Divinity Original 2 - Definitive Edition
|
||||
"01008c8012920000", // Dying Light Platinum Edition
|
||||
"0100d11013e6a000", // Eschatos
|
||||
"01001cc01b2d4000", // Goat Simulator 3
|
||||
"01003620068ea000", // Hand of Fate 2
|
||||
"0100f7e00c70e000", // Hogwarts Legacy
|
||||
@@ -194,15 +193,9 @@ namespace Ryujinx.Common
|
||||
"0100d71004694000", // Minecraft
|
||||
"01007430037f6000", // Monopoly
|
||||
"0100853015e86000", // No Man's Sky
|
||||
"0100f85014ed0000", // No More Heroes
|
||||
"0100463014ed4000", // No More Heroes 2
|
||||
"0100e570094e8000", // Owlboy
|
||||
"01007bb017812000", // Portal
|
||||
"0100abd01785c000", // Portal 2
|
||||
"01009f100bc52000", // Psikyo Collection 1
|
||||
"01009d400c4a8000", // Psikyo Collection 2
|
||||
"01008e200c5c2000", // Muse Dash
|
||||
"01005ff002e2a000", // Rayman Legends
|
||||
"01007820196a6000", // Red Dead Redemption
|
||||
"0100e8300a67a000", // Risk
|
||||
"01002f7013224000", // Rune Factory 5
|
||||
|
||||
@@ -22,11 +22,10 @@ namespace Ryujinx.Common.Utilities
|
||||
}
|
||||
|
||||
// "dumpable" attribute of the calling process
|
||||
private const int PR_GET_DUMPABLE = 3;
|
||||
private const int PR_SET_DUMPABLE = 4;
|
||||
|
||||
[LibraryImport("libc", SetLastError = true)]
|
||||
private static partial int prctl(int option, int arg2);
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int prctl(int option, int arg2);
|
||||
|
||||
public static void SetCoreDumpable(bool dumpable)
|
||||
{
|
||||
@@ -37,13 +36,5 @@ namespace Ryujinx.Common.Utilities
|
||||
Debug.Assert(result == 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Use the below line to display dumpable status in the console:
|
||||
// Console.WriteLine($"{OsUtils.IsCoreDumpable()}");
|
||||
public static bool IsCoreDumpable()
|
||||
{
|
||||
int result = prctl(PR_GET_DUMPABLE, 0);
|
||||
return result == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,8 +488,6 @@ namespace Ryujinx.HLE.FileSystem
|
||||
if (keyPaths.Length is 0)
|
||||
throw new FileNotFoundException($"Directory '{keysSource}' contained no '.keys' files.");
|
||||
|
||||
List<string> failedFiles = new();
|
||||
|
||||
foreach (string filePath in keyPaths)
|
||||
{
|
||||
try
|
||||
@@ -499,18 +497,15 @@ namespace Ryujinx.HLE.FileSystem
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, e.Message);
|
||||
failedFiles.Add(Path.GetFileName(filePath));
|
||||
continue;
|
||||
}
|
||||
|
||||
string destPath = Path.Combine(installDirectory, Path.GetFileName(filePath));
|
||||
|
||||
File.Copy(filePath, destPath, true);
|
||||
}
|
||||
if (File.Exists(destPath))
|
||||
File.Delete(destPath);
|
||||
|
||||
if (failedFiles.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to install the following key files: {string.Join(", ", failedFiles)}");
|
||||
File.Copy(filePath, destPath, true);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -523,6 +518,8 @@ namespace Ryujinx.HLE.FileSystem
|
||||
|
||||
FileInfo info = new(keysSource);
|
||||
|
||||
using FileStream file = File.OpenRead(keysSource);
|
||||
|
||||
if (info.Extension is not ".keys")
|
||||
throw new InvalidFirmwarePackageException("Input file extension is not .keys");
|
||||
|
||||
@@ -537,6 +534,10 @@ namespace Ryujinx.HLE.FileSystem
|
||||
|
||||
string dest = Path.Combine(installDirectory, info.Name);
|
||||
|
||||
if (File.Exists(dest))
|
||||
File.Delete(dest);
|
||||
|
||||
// overwrite: true seems to not work on its own? https://github.com/Ryubing/Issues/issues/189
|
||||
File.Copy(keysSource, dest, true);
|
||||
}
|
||||
|
||||
@@ -1058,7 +1059,7 @@ namespace Ryujinx.HLE.FileSystem
|
||||
}
|
||||
}
|
||||
|
||||
public static bool AreKeysAlreadyPresent(string pathToCheck)
|
||||
public static bool AreKeysAlredyPresent(string pathToCheck)
|
||||
{
|
||||
string[] fileNames = ["prod.keys", "title.keys", "console.keys", "dev.keys"];
|
||||
foreach (string file in fileNames)
|
||||
|
||||
@@ -20,7 +20,6 @@ using Ryujinx.HLE.HOS.Services.Mii;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager;
|
||||
using Ryujinx.HLE.HOS.Services.Nv;
|
||||
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
|
||||
using Ryujinx.HLE.HOS.Services.Pcv.Bpc;
|
||||
@@ -67,8 +66,6 @@ namespace Ryujinx.HLE.HOS
|
||||
|
||||
internal List<NfpDevice> NfpDevices { get; private set; }
|
||||
|
||||
internal List<NfcDevice> NfcDevices { get; private set; }
|
||||
|
||||
internal SmRegistry SmRegistry { get; private set; }
|
||||
|
||||
internal ServerBase SmServer { get; private set; }
|
||||
@@ -135,7 +132,6 @@ namespace Ryujinx.HLE.HOS
|
||||
PerformanceState = new PerformanceState();
|
||||
|
||||
NfpDevices = [];
|
||||
NfcDevices = [];
|
||||
|
||||
// Note: This is not really correct, but with HLE of services, the only memory
|
||||
// region used that is used is Application, so we can use the other ones for anything.
|
||||
@@ -246,21 +242,21 @@ namespace Ryujinx.HLE.HOS
|
||||
public void InitializeServices()
|
||||
{
|
||||
SmRegistry = new SmRegistry();
|
||||
SmServer = new ServerBase(KernelContext, "Sm", () => new IUserInterface(KernelContext, SmRegistry));
|
||||
SmServer = new ServerBase(KernelContext, "SmServer", () => new IUserInterface(KernelContext, SmRegistry));
|
||||
|
||||
// Wait until SM server thread is done with initialization,
|
||||
// only then doing connections to SM is safe.
|
||||
SmServer.InitDone.WaitOne();
|
||||
|
||||
BsdServer = new ServerBase(KernelContext, "Bsd");
|
||||
FsServer = new ServerBase(KernelContext, "Fs");
|
||||
HidServer = new ServerBase(KernelContext, "Hid");
|
||||
NvDrvServer = new ServerBase(KernelContext, "Nv");
|
||||
TimeServer = new ServerBase(KernelContext, "Time");
|
||||
ViServer = new ServerBase(KernelContext, "Vi:u");
|
||||
ViServerM = new ServerBase(KernelContext, "Vi:m");
|
||||
ViServerS = new ServerBase(KernelContext, "Vi:s");
|
||||
LdnServer = new ServerBase(KernelContext, "Ldn");
|
||||
BsdServer = new ServerBase(KernelContext, "BsdServer");
|
||||
FsServer = new ServerBase(KernelContext, "FsServer");
|
||||
HidServer = new ServerBase(KernelContext, "HidServer");
|
||||
NvDrvServer = new ServerBase(KernelContext, "NvservicesServer");
|
||||
TimeServer = new ServerBase(KernelContext, "TimeServer");
|
||||
ViServer = new ServerBase(KernelContext, "ViServerU");
|
||||
ViServerM = new ServerBase(KernelContext, "ViServerM");
|
||||
ViServerS = new ServerBase(KernelContext, "ViServerS");
|
||||
LdnServer = new ServerBase(KernelContext, "LdnServer");
|
||||
|
||||
StartNewServices();
|
||||
}
|
||||
@@ -286,7 +282,7 @@ namespace Ryujinx.HLE.HOS
|
||||
ProcessCreationFlags.Is64Bit |
|
||||
ProcessCreationFlags.PoolPartitionSystem;
|
||||
|
||||
ProcessCreationInfo creationInfo = new(service.Name, 1, 0, 0x8000000, 1, Flags, 0, 0);
|
||||
ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
|
||||
|
||||
uint[] defaultCapabilities =
|
||||
[
|
||||
@@ -376,15 +372,6 @@ namespace Ryujinx.HLE.HOS
|
||||
}
|
||||
}
|
||||
|
||||
public void ScanSkylander(int nfcDeviceId, byte[] data)
|
||||
{
|
||||
if (NfcDevices[nfcDeviceId].State == NfcDeviceState.SearchingForTag)
|
||||
{
|
||||
NfcDevices[nfcDeviceId].State = NfcDeviceState.TagFound;
|
||||
NfcDevices[nfcDeviceId].Data = data;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SearchingForAmiibo(out int nfpDeviceId)
|
||||
{
|
||||
nfpDeviceId = default;
|
||||
@@ -402,53 +389,6 @@ namespace Ryujinx.HLE.HOS
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool SearchingForSkylander(out int nfcDeviceId)
|
||||
{
|
||||
nfcDeviceId = default;
|
||||
|
||||
for (int i = 0; i < NfcDevices.Count; i++)
|
||||
{
|
||||
if (NfcDevices[i].State == NfcDeviceState.SearchingForTag)
|
||||
{
|
||||
nfcDeviceId = i;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool HasSkylander(out int nfcDeviceId)
|
||||
{
|
||||
nfcDeviceId = default;
|
||||
|
||||
for (int i = 0; i < NfcDevices.Count; i++)
|
||||
{
|
||||
if (NfcDevices[i].State == NfcDeviceState.TagFound)
|
||||
{
|
||||
nfcDeviceId = i;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void RemoveSkylander()
|
||||
{
|
||||
for (int i = 0; i < NfcDevices.Count; i++)
|
||||
{
|
||||
if (NfcDevices[i].State == NfcDeviceState.TagFound)
|
||||
{
|
||||
NfcDevices[i].State = NfcDeviceState.Initialized;
|
||||
NfcDevices[i].SignalDeactivate();
|
||||
Thread.Sleep(100); // NOTE: Simulate skylander scanning delay.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SignalDisplayResolutionChange()
|
||||
{
|
||||
DisplayResolutionChangeEvent.ReadableEvent.Signal();
|
||||
|
||||
@@ -118,11 +118,8 @@ namespace Ryujinx.HLE.HOS.Services.Caps
|
||||
}
|
||||
|
||||
// NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data.
|
||||
using SKBitmap bitmap = new(new SKImageInfo(1280, 720, SKColorType.Rgba8888, SKAlphaType.Premul));
|
||||
int dataLen = screenshotData.Length > bitmap.ByteCount ? bitmap.ByteCount : screenshotData.Length;
|
||||
|
||||
Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), dataLen);
|
||||
|
||||
using SKBitmap bitmap = new(new SKImageInfo(1280, 720, SKColorType.Rgba8888));
|
||||
Marshal.Copy(screenshotData, 0, bitmap.GetPixels(), screenshotData.Length);
|
||||
using SKData data = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
|
||||
using FileStream file = File.OpenWrite(filePath);
|
||||
data.SaveTo(file);
|
||||
|
||||
@@ -56,7 +56,6 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||
_activeCount = 0;
|
||||
|
||||
JoyHold = NpadJoyHoldType.Vertical;
|
||||
SixAxisActive = false;
|
||||
}
|
||||
|
||||
internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player)
|
||||
@@ -581,29 +580,6 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||
|
||||
return needUpdateRight;
|
||||
}
|
||||
|
||||
public bool isAtRest(int playerNumber)
|
||||
{
|
||||
ref NpadInternalState currentNpad = ref _device.Hid.SharedMemory.Npads[playerNumber].InternalState;
|
||||
|
||||
if (currentNpad.StyleSet == NpadStyleTag.None)
|
||||
{
|
||||
return true; // it will always be at rest because it cannot move.
|
||||
}
|
||||
|
||||
ref SixAxisSensorState storage = ref GetSixAxisSensorLifo(ref currentNpad, false).GetCurrentEntryRef();
|
||||
|
||||
float acceleration = Math.Abs(storage.Acceleration.X)
|
||||
+ Math.Abs(storage.Acceleration.Y)
|
||||
+ Math.Abs(storage.Acceleration.Z);
|
||||
|
||||
float angularVelocity = Math.Abs(storage.AngularVelocity.X)
|
||||
+ Math.Abs(storage.AngularVelocity.Y)
|
||||
+ Math.Abs(storage.AngularVelocity.Z);
|
||||
|
||||
// TODO: check against config deadzone and add sensitivity setting
|
||||
return ((acceleration <= 1.0F) && (angularVelocity <= 1.0F));
|
||||
}
|
||||
|
||||
private void UpdateDisconnectedInputSixAxis(PlayerIndex index)
|
||||
{
|
||||
|
||||
@@ -602,33 +602,19 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||
}
|
||||
|
||||
[CommandCmif(82)]
|
||||
// IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAtRest
|
||||
// IsSixAxisSensorAtRest(nn::hid::SixAxisSensorHandle, nn::applet::AppletResourceUserId) -> bool IsAsRest
|
||||
public ResultCode IsSixAxisSensorAtRest(ServiceCtx context)
|
||||
{
|
||||
int sixAxisSensorHandle = context.RequestData.ReadInt32();
|
||||
|
||||
// 4 byte struct w/ 4-byte alignment
|
||||
|
||||
// uint typeValue = (uint) sixAxisSensorHandle; // 0x0 0x4 TypeValue
|
||||
// uint npadStyleIndex = (uint) sixAxisSensorHandle & 0xff; // 0x0 0x1 NpadStyleIndex
|
||||
int playerNumber = (sixAxisSensorHandle << 8) & 0xff; // 0x1 0x1 PlayerNumber
|
||||
// uint deviceIdx= ((uint) sixAxisSensorHandle << 16) & 0xff; // 0x2 0x1 DeviceIdx
|
||||
// uint unknown = ((uint) sixAxisSensorHandle << 24) & 0xff;
|
||||
|
||||
// 32bit sign extension padding -> if = 0, + offset, else - offset
|
||||
|
||||
// npadStyleIndex = ((npadStyleIndex & 0x8000) == 0) ? npadStyleIndex | 0xFFFF0000 : npadStyleIndex & 0xFFFF0000;
|
||||
// playerNumber = ((playerNumber & 0x8000) == 0) ? playerNumber | 0xFFFF0000 : playerNumber & 0xFFFF0000;
|
||||
// deviceIdx = ((deviceIdx & 0x8000) == 0) ? deviceIdx | 0xFFFF0000 : deviceIdx & 0xFFFF0000;
|
||||
// unknown = ((unknown & 0x8000) == 0) ? unknown | 0xFFFF0000 : unknown & 0xFFFF0000;
|
||||
|
||||
context.RequestData.BaseStream.Position += 4; // Padding
|
||||
long appletResourceUserId = context.RequestData.ReadInt64();
|
||||
|
||||
// TODO: link to context.Device.Hid.Npads.SixAxisActive when properly implemented
|
||||
// We currently do not support stopping or starting SixAxisTracking.
|
||||
|
||||
context.ResponseData.Write(context.Device.Hid.Npads.isAtRest(playerNumber));
|
||||
|
||||
bool isAtRest = true;
|
||||
|
||||
context.ResponseData.Write(isAtRest);
|
||||
|
||||
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, isAtRest });
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
@@ -643,7 +629,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
||||
context.ResponseData.Write(_isFirmwareUpdateAvailableForSixAxisSensor);
|
||||
|
||||
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _isFirmwareUpdateAvailableForSixAxisSensor });
|
||||
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
using Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare
|
||||
{
|
||||
[Service("nfc:mf:u")]
|
||||
class IUserManager : IpcService
|
||||
{
|
||||
public IUserManager(ServiceCtx context) { }
|
||||
|
||||
[CommandCmif(0)]
|
||||
// CreateUserInterface() -> object<nn::nfc::mf::IUser>
|
||||
public ResultCode CreateUserInterface(ServiceCtx context)
|
||||
{
|
||||
MakeObject(context, new IMifare());
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,477 +0,0 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Cpu;
|
||||
using Ryujinx.HLE.HOS.Ipc;
|
||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using Ryujinx.HLE.HOS.Services.Hid.HidServer;
|
||||
using Ryujinx.Horizon.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
class IMifare : IpcService
|
||||
{
|
||||
private State _state;
|
||||
|
||||
private KEvent _availabilityChangeEvent;
|
||||
|
||||
private CancellationTokenSource _cancelTokenSource;
|
||||
|
||||
public IMifare()
|
||||
{
|
||||
_state = State.NonInitialized;
|
||||
}
|
||||
|
||||
[CommandCmif(0)]
|
||||
public ResultCode Initialize(ServiceCtx context)
|
||||
{
|
||||
_state = State.Initialized;
|
||||
|
||||
NfcDevice devicePlayer1 = new()
|
||||
{
|
||||
NpadIdType = NpadIdType.Player1,
|
||||
Handle = HidUtils.GetIndexFromNpadIdType(NpadIdType.Player1),
|
||||
State = NfcDeviceState.Initialized,
|
||||
};
|
||||
|
||||
context.Device.System.NfcDevices.Add(devicePlayer1);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(1)]
|
||||
public ResultCode Finalize(ServiceCtx context)
|
||||
{
|
||||
if (_state == State.Initialized)
|
||||
{
|
||||
_cancelTokenSource?.Cancel();
|
||||
|
||||
// NOTE: All events are destroyed here.
|
||||
context.Device.System.NfcDevices.Clear();
|
||||
|
||||
_state = State.NonInitialized;
|
||||
}
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(2)]
|
||||
public ResultCode GetListDevices(ServiceCtx context)
|
||||
{
|
||||
if (context.Request.RecvListBuff.Count == 0)
|
||||
{
|
||||
return ResultCode.WrongArgument;
|
||||
}
|
||||
|
||||
ulong outputPosition = context.Request.RecvListBuff[0].Position;
|
||||
ulong outputSize = context.Request.RecvListBuff[0].Size;
|
||||
|
||||
if (context.Device.System.NfcDevices.Count == 0)
|
||||
{
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize);
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
context.Memory.Write(outputPosition + ((uint)i * sizeof(long)), (uint)context.Device.System.NfcDevices[i].Handle);
|
||||
}
|
||||
|
||||
context.ResponseData.Write(context.Device.System.NfcDevices.Count);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(3)]
|
||||
public ResultCode StartDetection(ServiceCtx context)
|
||||
{
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
|
||||
{
|
||||
context.Device.System.NfcDevices[i].State = NfcDeviceState.SearchingForTag;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_cancelTokenSource = new CancellationTokenSource();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_cancelTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagFound)
|
||||
{
|
||||
context.Device.System.NfcDevices[i].SignalActivate();
|
||||
Thread.Sleep(125); // NOTE: Simulate skylander scanning delay.
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, _cancelTokenSource.Token);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(4)]
|
||||
public ResultCode StopDetection(ServiceCtx context)
|
||||
{
|
||||
_cancelTokenSource?.Cancel();
|
||||
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
|
||||
{
|
||||
context.Device.System.NfcDevices[i].State = NfcDeviceState.Initialized;
|
||||
Array.Clear(context.Device.System.NfcDevices[i].Data);
|
||||
context.Device.System.NfcDevices[i].SignalDeactivate();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(5)]
|
||||
public ResultCode ReadMifare(ServiceCtx context)
|
||||
{
|
||||
if (context.Request.ReceiveBuff.Count == 0 || context.Request.SendBuff.Count == 0)
|
||||
{
|
||||
return ResultCode.WrongArgument;
|
||||
}
|
||||
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
|
||||
if (context.Device.System.NfcDevices.Count == 0)
|
||||
{
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
ulong outputPosition = context.Request.ReceiveBuff[0].Position;
|
||||
ulong outputSize = context.Request.ReceiveBuff[0].Size;
|
||||
|
||||
MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize);
|
||||
|
||||
ulong inputPosition = context.Request.SendBuff[0].Position;
|
||||
ulong inputSize = context.Request.SendBuff[0].Size;
|
||||
|
||||
byte[] readBlockParameter = new byte[inputSize];
|
||||
|
||||
context.Memory.Read(inputPosition, readBlockParameter);
|
||||
|
||||
var span = MemoryMarshal.Cast<byte, NfcMifareReadBlockParameter>(readBlockParameter);
|
||||
var list = new List<NfcMifareReadBlockParameter>(span.Length);
|
||||
|
||||
foreach (var item in span)
|
||||
list.Add(item);
|
||||
|
||||
Thread.Sleep(125 * list.Count); // NOTE: Simulate skylander scanning delay.
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved)
|
||||
{
|
||||
return ResultCode.TagNotFound;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int p = 0; p < list.Count; p++)
|
||||
{
|
||||
NfcMifareReadBlockData blockData = new()
|
||||
{
|
||||
SectorNumber = list[p].SectorNumber,
|
||||
Reserved = new Array7<byte>(),
|
||||
};
|
||||
byte[] data = new byte[16];
|
||||
|
||||
switch (list[p].SectorKey.MifareCommand)
|
||||
{
|
||||
case NfcMifareCommand.NfcMifareCommand_Read:
|
||||
case NfcMifareCommand.NfcMifareCommand_AuthA:
|
||||
if (IsCurrentBlockKeyBlock(list[p].SectorNumber))
|
||||
{
|
||||
Array.Copy(context.Device.System.NfcDevices[i].Data, (16 * list[p].SectorNumber) + 6, data, 6, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(context.Device.System.NfcDevices[i].Data, 16 * list[p].SectorNumber, data, 0, 16);
|
||||
}
|
||||
data.CopyTo(blockData.Data.AsSpan());
|
||||
context.Memory.Write(outputPosition + ((uint)(p * Unsafe.SizeOf<NfcMifareReadBlockData>())), blockData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(6)]
|
||||
public ResultCode WriteMifare(ServiceCtx context)
|
||||
{
|
||||
if (context.Request.SendBuff.Count == 0)
|
||||
{
|
||||
return ResultCode.WrongArgument;
|
||||
}
|
||||
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
|
||||
if (context.Device.System.NfcDevices.Count == 0)
|
||||
{
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
ulong inputPosition = context.Request.SendBuff[0].Position;
|
||||
ulong inputSize = context.Request.SendBuff[0].Size;
|
||||
|
||||
byte[] writeBlockParameter = new byte[inputSize];
|
||||
|
||||
context.Memory.Read(inputPosition, writeBlockParameter);
|
||||
|
||||
var span = MemoryMarshal.Cast<byte, NfcMifareWriteBlockParameter>(writeBlockParameter);
|
||||
var list = new List<NfcMifareWriteBlockParameter>(span.Length);
|
||||
|
||||
foreach (var item in span)
|
||||
list.Add(item);
|
||||
|
||||
Thread.Sleep(125 * list.Count); // NOTE: Simulate skylander scanning delay.
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved)
|
||||
{
|
||||
return ResultCode.TagNotFound;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int p = 0; p < list.Count; p++)
|
||||
{
|
||||
switch (list[p].SectorKey.MifareCommand)
|
||||
{
|
||||
case NfcMifareCommand.NfcMifareCommand_Write:
|
||||
case NfcMifareCommand.NfcMifareCommand_AuthA:
|
||||
list[p].Data.AsSpan().CopyTo(context.Device.System.NfcDevices[i].Data.AsSpan(list[p].SectorNumber * 16, 16));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(7)]
|
||||
public ResultCode GetTagInfo(ServiceCtx context)
|
||||
{
|
||||
ResultCode resultCode = ResultCode.Success;
|
||||
|
||||
if (context.Request.RecvListBuff.Count == 0)
|
||||
{
|
||||
return ResultCode.WrongArgument;
|
||||
}
|
||||
|
||||
ulong outputPosition = context.Request.RecvListBuff[0].Position;
|
||||
|
||||
context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize((uint)Marshal.SizeOf<TagInfo>());
|
||||
|
||||
MemoryHelper.FillWithZeros(context.Memory, outputPosition, Marshal.SizeOf<TagInfo>());
|
||||
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
|
||||
if (context.Device.System.NfcDevices.Count == 0)
|
||||
{
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved)
|
||||
{
|
||||
resultCode = ResultCode.TagNotFound;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagMounted || context.Device.System.NfcDevices[i].State == NfcDeviceState.TagFound)
|
||||
{
|
||||
TagInfo tagInfo = new()
|
||||
{
|
||||
UuidLength = 4,
|
||||
Reserved1 = new Array21<byte>(),
|
||||
Protocol = (uint)NfcProtocol.NfcProtocol_TypeA, // Type A Protocol
|
||||
TagType = (uint)NfcTagType.NfcTagType_Mifare, // Mifare Type
|
||||
Reserved2 = new Array6<byte>(),
|
||||
};
|
||||
|
||||
byte[] uuid = new byte[4];
|
||||
|
||||
Array.Copy(context.Device.System.NfcDevices[i].Data, 0, uuid, 0, 4);
|
||||
|
||||
uuid.CopyTo(tagInfo.Uuid.AsSpan());
|
||||
|
||||
context.Memory.Write(outputPosition, tagInfo);
|
||||
|
||||
resultCode = ResultCode.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
resultCode = ResultCode.WrongDeviceState;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return resultCode;
|
||||
}
|
||||
|
||||
[CommandCmif(8)]
|
||||
public ResultCode AttachActivateEvent(ServiceCtx context)
|
||||
{
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle)
|
||||
{
|
||||
context.Device.System.NfcDevices[i].ActivateEvent = new KEvent(context.Device.System.KernelContext);
|
||||
|
||||
if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfcDevices[i].ActivateEvent.ReadableEvent, out int activateEventHandle) != Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException("Out of handles!");
|
||||
}
|
||||
|
||||
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(activateEventHandle);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
}
|
||||
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
[CommandCmif(9)]
|
||||
public ResultCode AttachDeactivateEvent(ServiceCtx context)
|
||||
{
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle)
|
||||
{
|
||||
context.Device.System.NfcDevices[i].DeactivateEvent = new KEvent(context.Device.System.KernelContext);
|
||||
|
||||
if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfcDevices[i].DeactivateEvent.ReadableEvent, out int deactivateEventHandle) != Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException("Out of handles!");
|
||||
}
|
||||
|
||||
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(deactivateEventHandle);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
}
|
||||
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
[CommandCmif(10)]
|
||||
public ResultCode GetState(ServiceCtx context)
|
||||
{
|
||||
context.ResponseData.Write((int)_state);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(11)]
|
||||
public ResultCode GetDeviceState(ServiceCtx context)
|
||||
{
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle)
|
||||
{
|
||||
if (context.Device.System.NfcDevices[i].State > NfcDeviceState.Finalized)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(context.Device.System.NfcDevices)} contains an invalid state for device {i}: {context.Device.System.NfcDevices[i].State}");
|
||||
}
|
||||
context.ResponseData.Write((uint)context.Device.System.NfcDevices[i].State);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
}
|
||||
|
||||
context.ResponseData.Write((uint)NfcDeviceState.Unavailable);
|
||||
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
[CommandCmif(12)]
|
||||
public ResultCode GetNpadId(ServiceCtx context)
|
||||
{
|
||||
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
|
||||
|
||||
for (int i = 0; i < context.Device.System.NfcDevices.Count; i++)
|
||||
{
|
||||
if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle)
|
||||
{
|
||||
context.ResponseData.Write((uint)HidUtils.GetNpadIdTypeFromIndex(context.Device.System.NfcDevices[i].Handle));
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
}
|
||||
|
||||
return ResultCode.DeviceNotFound;
|
||||
}
|
||||
|
||||
[CommandCmif(13)]
|
||||
public ResultCode AttachAvailabilityChangeEvent(ServiceCtx context)
|
||||
{
|
||||
_availabilityChangeEvent = new KEvent(context.Device.System.KernelContext);
|
||||
|
||||
if (context.Process.HandleTable.GenerateHandle(_availabilityChangeEvent.ReadableEvent, out int availabilityChangeEventHandle) != Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException("Out of handles!");
|
||||
}
|
||||
|
||||
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(availabilityChangeEventHandle);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
private bool IsCurrentBlockKeyBlock(byte block)
|
||||
{
|
||||
return ((block + 1) % 4) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
class NfcDevice
|
||||
{
|
||||
public KEvent ActivateEvent;
|
||||
public KEvent DeactivateEvent;
|
||||
|
||||
public void SignalActivate() => ActivateEvent.ReadableEvent.Signal();
|
||||
public void SignalDeactivate() => DeactivateEvent.ReadableEvent.Signal();
|
||||
|
||||
public NfcDeviceState State = NfcDeviceState.Unavailable;
|
||||
|
||||
public PlayerIndex Handle;
|
||||
public NpadIdType NpadIdType;
|
||||
|
||||
public byte[] Data;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
enum NfcMifareCommand : byte
|
||||
{
|
||||
NfcMifareCommand_Read = 0x30,
|
||||
NfcMifareCommand_AuthA = 0x60,
|
||||
NfcMifareCommand_AuthB = 0x61,
|
||||
NfcMifareCommand_Write = 0xA0,
|
||||
NfcMifareCommand_Transfer = 0xB0,
|
||||
NfcMifareCommand_Decrement = 0xC0,
|
||||
NfcMifareCommand_Increment = 0xC1,
|
||||
NfcMifareCommand_Store = 0xC2,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x18)]
|
||||
struct NfcMifareReadBlockData
|
||||
{
|
||||
public Array16<byte> Data;
|
||||
public byte SectorNumber;
|
||||
public Array7<byte> Reserved;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x18)]
|
||||
struct NfcMifareReadBlockParameter
|
||||
{
|
||||
public byte SectorNumber;
|
||||
public Array7<byte> Reserved;
|
||||
public NfcSectorKey SectorKey;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x28)]
|
||||
struct NfcMifareWriteBlockParameter
|
||||
{
|
||||
public Array16<byte> Data;
|
||||
public byte SectorNumber;
|
||||
public Array7<byte> Reserved;
|
||||
public NfcSectorKey SectorKey;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
enum NfcProtocol : byte
|
||||
{
|
||||
NfcProtocol_None = 0b_0000_0000,
|
||||
NfcProtocol_TypeA = 0b_0000_0001, ///< ISO14443A
|
||||
NfcProtocol_TypeB = 0b_0000_0010, ///< ISO14443B
|
||||
NfcProtocol_TypeF = 0b_0000_0100, ///< Sony FeliCa
|
||||
NfcProtocol_All = 0xFF,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
|
||||
struct NfcSectorKey
|
||||
{
|
||||
public NfcMifareCommand MifareCommand;
|
||||
public byte Unknown;
|
||||
public Array6<byte> Reserved1;
|
||||
public Array6<byte> SectorKey;
|
||||
public Array2<byte> Reserved2;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
enum NfcTagType : byte
|
||||
{
|
||||
NfcTagType_None = 0b_0000_0000,
|
||||
NfcTagType_Type1 = 0b_0000_0001, ///< ISO14443A RW. Topaz
|
||||
NfcTagType_Type2 = 0b_0000_0010, ///< ISO14443A RW. Ultralight, NTAGX, ST25TN
|
||||
NfcTagType_Type3 = 0b_0000_0100, ///< ISO14443A RW/RO. Sony FeliCa
|
||||
NfcTagType_Type4A = 0b_0000_1000, ///< ISO14443A RW/RO. DESFire
|
||||
NfcTagType_Type4B = 0b_0001_0000, ///< ISO14443B RW/RO. DESFire
|
||||
NfcTagType_Type5 = 0b_0010_0000, ///< ISO15693 RW/RO. SLI, SLIX, ST25TV
|
||||
NfcTagType_Mifare = 0b_0100_0000, ///< Mifare clasic. Skylanders
|
||||
NfcTagType_All = 0xFF,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
enum NfcDeviceState : byte
|
||||
{
|
||||
Initialized = 0,
|
||||
SearchingForTag = 1,
|
||||
TagFound = 2,
|
||||
TagRemoved = 3,
|
||||
TagMounted = 4,
|
||||
Unavailable = 5,
|
||||
Finalized = 6,
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
enum State
|
||||
{
|
||||
NonInitialized,
|
||||
Initialized,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x58)]
|
||||
struct TagInfo
|
||||
{
|
||||
public Array10<byte> Uuid;
|
||||
public byte UuidLength;
|
||||
public Array21<byte> Reserved1;
|
||||
public uint Protocol;
|
||||
public uint TagType;
|
||||
public Array6<byte> Reserved2;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare
|
||||
{
|
||||
public enum ResultCode
|
||||
{
|
||||
ModuleId = 161,
|
||||
ErrorCodeShift = 9,
|
||||
|
||||
Success = 0,
|
||||
|
||||
DeviceNotFound = (64 << ErrorCodeShift) | ModuleId, // 0x80A1
|
||||
WrongArgument = (65 << ErrorCodeShift) | ModuleId, // 0x82A1
|
||||
WrongDeviceState = (73 << ErrorCodeShift) | ModuleId, // 0x92A1
|
||||
NfcDisabled = (80 << ErrorCodeShift) | ModuleId, // 0xA0A1
|
||||
TagNotFound = (97 << ErrorCodeShift) | ModuleId, // 0xC2A1
|
||||
MifareAccessError = (288 << ErrorCodeShift) | ModuleId, // 0x240a1
|
||||
}
|
||||
}
|
||||
@@ -79,15 +79,9 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
ProcessCreationFlags.Is64Bit |
|
||||
ProcessCreationFlags.PoolPartitionSystem;
|
||||
|
||||
ProcessCreationInfo creationInfo = new(Name, 1, 0, 0x8000000, 1, Flags, 0, 0);
|
||||
ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
|
||||
|
||||
KernelStatic.StartInitialProcess(context, creationInfo, DefaultCapabilities, 44, () =>
|
||||
{
|
||||
var currentThread = KernelStatic.GetCurrentThread();
|
||||
currentThread.HostThread.Name = $"{{{Name}}}";
|
||||
|
||||
Main();
|
||||
});
|
||||
KernelStatic.StartInitialProcess(context, creationInfo, DefaultCapabilities, 44, Main);
|
||||
}
|
||||
|
||||
private void AddPort(int serverPortHandle, Func<IpcService> objectFactory)
|
||||
|
||||
@@ -17,12 +17,13 @@ namespace Ryujinx.HLE.HOS.Services.Sm
|
||||
private static readonly Dictionary<string, Type> _services;
|
||||
|
||||
private readonly SmRegistry _registry;
|
||||
private ServerBase _commonServer;
|
||||
private readonly ServerBase _commonServer;
|
||||
|
||||
private bool _isInitialized;
|
||||
|
||||
public IUserInterface(KernelContext context, SmRegistry registry) : base(registerTipc: true)
|
||||
{
|
||||
_commonServer = new ServerBase(context, "CommonServer");
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
@@ -96,11 +97,6 @@ namespace Ryujinx.HLE.HOS.Services.Sm
|
||||
|
||||
IpcService service = GetServiceInstance(type, context, serviceAttribute.Parameter);
|
||||
|
||||
if (_commonServer is null)
|
||||
{
|
||||
_commonServer = new ServerBase(context.Device.System.KernelContext, "Common");
|
||||
}
|
||||
|
||||
service.TrySetServer(_commonServer);
|
||||
service.Server.AddSessionObj(session.ServerSession, service);
|
||||
}
|
||||
@@ -257,7 +253,7 @@ namespace Ryujinx.HLE.HOS.Services.Sm
|
||||
|
||||
public override void DestroyAtExit()
|
||||
{
|
||||
_commonServer?.Dispose();
|
||||
_commonServer.Dispose();
|
||||
|
||||
base.DestroyAtExit();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl
|
||||
public ISslService(ServiceCtx context) { }
|
||||
|
||||
[CommandCmif(0)]
|
||||
// CreateContext(nn::ssl::sf::SslVersion, u64 pid_placeholder, pid) -> object<nn::ssl::sf::ISslContext>
|
||||
// CreateContext(nn::ssl::sf::SslVersion, u64, pid) -> object<nn::ssl::sf::ISslContext>
|
||||
public ResultCode CreateContext(ServiceCtx context)
|
||||
{
|
||||
SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32();
|
||||
@@ -126,18 +126,14 @@ namespace Ryujinx.HLE.HOS.Services.Ssl
|
||||
}
|
||||
|
||||
[CommandCmif(100)]
|
||||
// CreateContextForSystem(nn::ssl::sf::SslVersion, u64 pid_placeholder, pid) -> object<nn::ssl::sf::ISslContextForSystem>
|
||||
// CreateContextForSystem(u64 pid, nn::ssl::sf::SslVersion, u64)
|
||||
public ResultCode CreateContextForSystem(ServiceCtx context)
|
||||
{
|
||||
ulong pid = context.RequestData.ReadUInt64();
|
||||
SslVersion sslVersion = (SslVersion)context.RequestData.ReadUInt32();
|
||||
#pragma warning disable IDE0059 // Remove unnecessary value assignment
|
||||
ulong pidPlaceholder = context.RequestData.ReadUInt64();
|
||||
#pragma warning restore IDE0059
|
||||
|
||||
// Note: We use ISslContext here instead of ISslContextForSystem class because Ryujinx implements both in one class.
|
||||
MakeObject(context, new ISslContext(context.Request.HandleDesc.PId, sslVersion));
|
||||
|
||||
Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { sslVersion });
|
||||
Logger.Stub?.PrintStub(LogClass.ServiceSsl, new { pid, sslVersion, pidPlaceholder });
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,5 @@ namespace Ryujinx.HLE.HOS.SystemState
|
||||
SimplifiedChinese,
|
||||
TraditionalChinese,
|
||||
BrazilianPortuguese,
|
||||
Polish,
|
||||
Thai,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ namespace Ryujinx.HLE.HOS.SystemState
|
||||
"es-419",
|
||||
"zh-Hans",
|
||||
"zh-Hant",
|
||||
"pt-BR",
|
||||
"pl",
|
||||
"th"
|
||||
"pt-BR"
|
||||
];
|
||||
|
||||
internal long DesiredKeyboardLayout { get; private set; }
|
||||
|
||||
@@ -18,7 +18,5 @@ namespace Ryujinx.HLE.HOS.SystemState
|
||||
TraditionalChinese,
|
||||
SimplifiedChinese,
|
||||
BrazilianPortuguese,
|
||||
Polish,
|
||||
Thai,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
using Ryujinx.Common.Memory;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Horizon.Sdk.Ns
|
||||
{
|
||||
public struct ApplicationControlProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// Use <see cref="Title"/> to access titles instead of accessing them directly.
|
||||
/// </summary>
|
||||
public Array16<ApplicationTitle> TitleBlock;
|
||||
public Array16<ApplicationTitle> Title;
|
||||
public Array37<byte> Isbn;
|
||||
public StartupUserAccountValue StartupUserAccount;
|
||||
public UserAccountSwitchLockValue UserAccountSwitchLock;
|
||||
@@ -65,10 +58,7 @@ namespace Ryujinx.Horizon.Sdk.Ns
|
||||
public RepairFlagValue RepairFlag;
|
||||
public byte ProgramIndex;
|
||||
public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag;
|
||||
public byte ApplicationErrorCodePrefix;
|
||||
public TitleCompressionValue TitleCompression;
|
||||
public byte AcdIndex;
|
||||
public byte ApparentPlatform;
|
||||
public Array4<byte> Reserved3214;
|
||||
public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration;
|
||||
public ApplicationJitConfiguration JitConfiguration;
|
||||
public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors;
|
||||
@@ -84,47 +74,6 @@ namespace Ryujinx.Horizon.Sdk.Ns
|
||||
public readonly string DisplayVersionString => Encoding.UTF8.GetString(DisplayVersion.AsSpan()).TrimEnd('\0');
|
||||
public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0');
|
||||
public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0');
|
||||
|
||||
private const int TitleCount = 32;
|
||||
private const int TitleEntrySize = 0x300;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the resolved title entries. When <see cref="TitleCompression"/> is
|
||||
/// <see cref="TitleCompressionValue.Enable"/>, the raw <see cref="TitleBlock"/> bytes are
|
||||
/// decompressed (raw deflate) from 0x3000 into 0x6000 bytes yielding up to 32 entries.
|
||||
/// Otherwise the 16 uncompressed entries from <see cref="TitleBlock"/> are returned directly.
|
||||
/// </summary>
|
||||
public readonly ApplicationTitle[] Title
|
||||
{
|
||||
get
|
||||
{
|
||||
var titles = new ApplicationTitle[TitleCount];
|
||||
|
||||
if (TitleCompression != TitleCompressionValue.Enable)
|
||||
{
|
||||
TitleBlock.AsSpan().CopyTo(titles);
|
||||
|
||||
return titles;
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> titleBytes = MemoryMarshal.AsBytes(TitleBlock.AsSpan());
|
||||
ushort compressedBlobSize = BinaryPrimitives.ReadUInt16LittleEndian(titleBytes);
|
||||
ReadOnlySpan<byte> compressedBlob = titleBytes.Slice(2, compressedBlobSize);
|
||||
|
||||
byte[] decompressed = new byte[TitleCount * TitleEntrySize];
|
||||
|
||||
using (var compressedStream = new MemoryStream(compressedBlob.ToArray()))
|
||||
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
|
||||
{
|
||||
deflateStream.ReadExactly(decompressed, 0, decompressed.Length);
|
||||
}
|
||||
|
||||
MemoryMarshal.Cast<byte, ApplicationTitle>(decompressed).CopyTo(titles);
|
||||
|
||||
return titles;
|
||||
}
|
||||
}
|
||||
|
||||
public struct ApplicationTitle
|
||||
{
|
||||
@@ -181,8 +130,6 @@ namespace Ryujinx.Horizon.Sdk.Ns
|
||||
TraditionalChinese = 13,
|
||||
SimplifiedChinese = 14,
|
||||
BrazilianPortuguese = 15,
|
||||
Polish = 16,
|
||||
Thai = 17,
|
||||
}
|
||||
|
||||
public enum Organization
|
||||
@@ -355,11 +302,5 @@ namespace Ryujinx.Horizon.Sdk.Ns
|
||||
Deny = 0,
|
||||
Allow = 1,
|
||||
}
|
||||
|
||||
public enum TitleCompressionValue : byte
|
||||
{
|
||||
Disable = 0,
|
||||
Enable = 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,12 @@ namespace Ryujinx.Horizon
|
||||
private readonly Action<ServiceTable> _entrypoint;
|
||||
private readonly ServiceTable _serviceTable;
|
||||
private readonly HorizonOptions _options;
|
||||
public readonly string Name;
|
||||
|
||||
internal ServiceEntry(Action<ServiceTable> entrypoint, ServiceTable serviceTable, HorizonOptions options, string name)
|
||||
internal ServiceEntry(Action<ServiceTable> entrypoint, ServiceTable serviceTable, HorizonOptions options)
|
||||
{
|
||||
_entrypoint = entrypoint;
|
||||
_serviceTable = serviceTable;
|
||||
_options = options;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public void Start(ISyscallApi syscallApi, IVirtualMemoryManager addressSpace, IThreadContext threadContext)
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Ryujinx.Horizon
|
||||
|
||||
void RegisterService<T>() where T : IService
|
||||
{
|
||||
entries.Add(new ServiceEntry(T.Main, this, options, typeof(T).Name));
|
||||
entries.Add(new ServiceEntry(T.Main, this, options));
|
||||
}
|
||||
|
||||
RegisterService<ArpMain>();
|
||||
|
||||
@@ -151,9 +151,7 @@ namespace Ryujinx.Input.SDL3
|
||||
result |= GamepadFeaturesFlag.Led;
|
||||
}
|
||||
SDL_UnlockProperties(propID);
|
||||
|
||||
// NOTE: Do not call SDL_DestroyProperties here. These properties are owned
|
||||
// internally by SDL and are freed when SDL_CloseGamepad is called (in Dispose).
|
||||
SDL_DestroyProperties(propID);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -331,18 +331,28 @@ namespace Ryujinx.Input.SDL3
|
||||
|
||||
public IEnumerable<IGamepad> GetGamepads()
|
||||
{
|
||||
string[] ids;
|
||||
lock (_lock)
|
||||
lock (_gamepadsIds)
|
||||
{
|
||||
ids = _gamepadsIds.Values
|
||||
.Concat(_joyConsIds.Values)
|
||||
.Concat(_linkedJoyConsIds.Values)
|
||||
.ToArray();
|
||||
foreach (var gamepad in _gamepadsIds)
|
||||
{
|
||||
yield return GetGamepad(gamepad.Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string id in ids)
|
||||
lock (_joyConsIds)
|
||||
{
|
||||
yield return GetGamepad(id);
|
||||
foreach (var gamepad in _joyConsIds)
|
||||
{
|
||||
yield return GetGamepad(gamepad.Value);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_linkedJoyConsIds)
|
||||
{
|
||||
foreach (var gamepad in _linkedJoyConsIds)
|
||||
{
|
||||
yield return GetGamepad(gamepad.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ namespace Ryujinx.Input.HLE
|
||||
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
|
||||
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
|
||||
|
||||
_gamepad?.Rumble(low, high, uint.MaxValue);
|
||||
_gamepad.Rumble(low, high, uint.MaxValue);
|
||||
|
||||
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
|
||||
$"L.low.amp={leftVibrationValue.AmplitudeLow}, " +
|
||||
|
||||
@@ -61,14 +61,6 @@ namespace Ryujinx.SDL3.Common
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1");
|
||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||
|
||||
// When hid_nintendo is loaded, it creates separate evdev devices for the gamepad
|
||||
// and IMU which SDL3's evdev backend combines via UNIQ matching. Using HIDAPI
|
||||
// instead conflicts with the kernel driver and breaks motion and hotplug.
|
||||
if (OperatingSystem.IsLinux() && Directory.Exists("/sys/module/hid_nintendo"))
|
||||
{
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "0");
|
||||
}
|
||||
|
||||
// NOTE: As of SDL3 2.24.0, joycons are combined by default but the motion source only come from one of them.
|
||||
// We disable this behavior for now.
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "0");
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Ryujinx.Tests.Audio.Renderer.Parameter.Effect
|
||||
public void EnsureTypeSize()
|
||||
{
|
||||
Assert.AreEqual(0x18, Unsafe.SizeOf<BiquadFilterEffectParameter1>());
|
||||
Assert.AreEqual(0x28, Unsafe.SizeOf<BiquadFilterEffectParameter2>());
|
||||
Assert.AreEqual(0x24, Unsafe.SizeOf<BiquadFilterEffectParameter2>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
src/Ryujinx/Assets/UIImages/Logo_GitLab_Dark.png
Normal file
BIN
src/Ryujinx/Assets/UIImages/Logo_GitLab_Dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/Ryujinx/Assets/UIImages/Logo_GitLab_Light.png
Normal file
BIN
src/Ryujinx/Assets/UIImages/Logo_GitLab_Light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
@@ -24,11 +24,9 @@ using Ryujinx.Headless;
|
||||
using Ryujinx.SDL3.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava
|
||||
@@ -44,7 +42,6 @@ namespace Ryujinx.Ava
|
||||
public static bool PreviewerDetached { get; private set; }
|
||||
public static bool UseHardwareAcceleration { get; private set; }
|
||||
public static string BackendThreadingArg { get; private set; }
|
||||
public static bool CoreDumpArg { get; private set; }
|
||||
|
||||
private const uint MbIconwarning = 0x30;
|
||||
|
||||
@@ -54,17 +51,6 @@ namespace Ryujinx.Ava
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
#if !DEBUG
|
||||
// this fixes the "hide console" option by forcing the emulator to launch in an old-school cmd
|
||||
if (!Console.Title.Contains("conhost.exe"))
|
||||
{
|
||||
string sargs = string.Join(" ", args);
|
||||
|
||||
Process.Start("conhost.exe", $"{Environment.ProcessPath} {sargs}");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
|
||||
{
|
||||
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
|
||||
@@ -95,8 +81,6 @@ namespace Ryujinx.Ava
|
||||
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
|
||||
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
|
||||
|
||||
CoreDumpArg = coreDumpArg;
|
||||
|
||||
// TODO: Ryujinx causes core dumps on Linux when it exits "uncleanly", eg. through an unhandled exception.
|
||||
// This is undesirable and causes very odd behavior during development (the process stops responding,
|
||||
// the .NET debugger freezes or suddenly detaches, /tmp/ gets filled etc.), unless explicitly requested by the user.
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>partial</TrimMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-arm64'">
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
FluentAvalonia, used in the Avalonia UI, requires a workaround for the json serializer used internally when using .NET 8+ System.Text.Json.
|
||||
@@ -46,11 +51,10 @@
|
||||
<PackageReference Include="Avalonia.Diagnostics" Condition="'$(Configuration)'=='Debug'" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" />
|
||||
<PackageReference Include="Avalonia.Markup.Xaml.Loader" />
|
||||
<PackageReference Include="SharpCompress" />
|
||||
<PackageReference Include="Svg.Controls.Avalonia" />
|
||||
<PackageReference Include="Svg.Controls.Skia.Avalonia" />
|
||||
<PackageReference Include="DynamicData" />
|
||||
<PackageReference Include="FluentAvaloniaUI" />
|
||||
<PackageReference Include="FluentAvaloniaUI.NoAnim" />
|
||||
<PackageReference Include="CommandLineParser" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="DiscordRichPresence" />
|
||||
@@ -69,6 +73,7 @@
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||
<PackageReference Include="SPB" />
|
||||
<PackageReference Include="SharpZipLib" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -169,8 +174,9 @@
|
||||
<EmbeddedResource Include="Assets\UIImages\Logo_Amiibo.png" />
|
||||
<EmbeddedResource Include="Assets\UIImages\Logo_Discord_Dark.png" />
|
||||
<EmbeddedResource Include="Assets\UIImages\Logo_Discord_Light.png" />
|
||||
<EmbeddedResource Include="Assets\UIImages\Logo_GitLab_Dark.png" />
|
||||
<EmbeddedResource Include="Assets\UIImages\Logo_GitLab_Light.png" />
|
||||
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx.png" />
|
||||
<EmbeddedResource Include="Assets\UIImages\Logo_Forgejo.png" />
|
||||
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1404,7 +1404,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
|
||||
if (string.IsNullOrWhiteSpace(data.Name))
|
||||
{
|
||||
foreach (ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
|
||||
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
|
||||
{
|
||||
if (!controlTitle.NameString.IsEmpty())
|
||||
{
|
||||
@@ -1417,7 +1417,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
|
||||
|
||||
if (string.IsNullOrWhiteSpace(data.Developer))
|
||||
{
|
||||
foreach (ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
|
||||
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
|
||||
{
|
||||
if (!controlTitle.PublisherString.IsEmpty())
|
||||
{
|
||||
|
||||
@@ -24,8 +24,6 @@ namespace Ryujinx.Ava.Systems.Configuration.System
|
||||
SimplifiedChinese,
|
||||
TraditionalChinese,
|
||||
BrazilianPortuguese,
|
||||
Polish,
|
||||
Thai,
|
||||
}
|
||||
|
||||
public static class LanguageEnumHelper
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace Ryujinx.Ava.Systems
|
||||
}
|
||||
|
||||
// If build URL not found, assume no new update is available.
|
||||
if (string.IsNullOrEmpty(_versionResponse.ArtifactUrl))
|
||||
if (_versionResponse.ArtifactUrl is null or "")
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
@@ -123,8 +123,6 @@ namespace Ryujinx.Ava.Systems
|
||||
return default;
|
||||
}
|
||||
|
||||
_connectionCount = (int)_versionResponse.MaxConcurrency;
|
||||
|
||||
return (currentVersion, newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Gommon;
|
||||
using ICSharpCode.SharpZipLib.GZip;
|
||||
using ICSharpCode.SharpZipLib.Tar;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Compressors.Xz;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Formats.Tar;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
@@ -22,6 +21,7 @@ using System.Net.NetworkInformation;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -32,8 +32,8 @@ namespace Ryujinx.Ava.Systems
|
||||
private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
||||
private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish");
|
||||
private const int ConnectionCount = 4;
|
||||
|
||||
private static int _connectionCount = 1;
|
||||
private static long _buildSize;
|
||||
private static bool _updateSuccessful;
|
||||
private static bool _running;
|
||||
@@ -73,6 +73,27 @@ namespace Ryujinx.Ava.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch build size information to learn chunk sizes.
|
||||
using HttpClient buildSizeClient = ConstructHttpClient();
|
||||
try
|
||||
{
|
||||
buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
|
||||
|
||||
// GitLab instance is located in Ukraine. Connection times will vary across the world.
|
||||
buildSizeClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_versionResponse.ArtifactUrl), HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
_buildSize = message.Content.Headers.ContentRange.Length.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
||||
Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater");
|
||||
|
||||
_buildSize = -1;
|
||||
}
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
string newVersionString = ReleaseInformation.IsCanaryBuild
|
||||
@@ -122,14 +143,6 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
Directory.CreateDirectory(_updateDir);
|
||||
|
||||
// If we get a .zip url switch it to the preferred .7z file instead
|
||||
// The update server still returns the .zip url by default for legacy support
|
||||
downloadUrl = downloadUrl.Replace(".zip", ".7z");
|
||||
|
||||
// If we get a .tar.gz url switch it to the preferred .tar.xz file instead
|
||||
// The update server still returns the .tar.gz url by default for legacy support
|
||||
downloadUrl = downloadUrl.Replace(".tar.gz", ".tar.xz");
|
||||
|
||||
string updateFile = Path.Combine(_updateDir, "update.bin");
|
||||
|
||||
TaskDialog taskDialog = new()
|
||||
@@ -140,27 +153,6 @@ namespace Ryujinx.Ava.Systems
|
||||
ShowProgressBar = true,
|
||||
XamlRoot = RyujinxApp.MainWindow,
|
||||
};
|
||||
|
||||
// Fetch build size information to learn chunk sizes.
|
||||
using HttpClient buildSizeClient = ConstructHttpClient();
|
||||
try
|
||||
{
|
||||
buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
|
||||
|
||||
// Forgejo instance is located in Ukraine. Connection times will vary across the world.
|
||||
buildSizeClient.Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_versionResponse.ArtifactUrl), HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
_buildSize = message.Content.Headers.ContentRange.Length.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
||||
Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater");
|
||||
|
||||
_buildSize = -1;
|
||||
}
|
||||
|
||||
taskDialog.Opened += (s, e) =>
|
||||
{
|
||||
@@ -242,22 +234,22 @@ namespace Ryujinx.Ava.Systems
|
||||
private static void DoUpdateWithMultipleThreads(TaskDialog taskDialog, string downloadUrl, string updateFile)
|
||||
{
|
||||
// Multi-Threaded Updater
|
||||
long chunkSize = _buildSize / _connectionCount;
|
||||
long remainderChunk = _buildSize % _connectionCount;
|
||||
long chunkSize = _buildSize / ConnectionCount;
|
||||
long remainderChunk = _buildSize % ConnectionCount;
|
||||
|
||||
int completedRequests = 0;
|
||||
int totalProgressPercentage = 0;
|
||||
int[] progressPercentage = new int[_connectionCount];
|
||||
int[] progressPercentage = new int[ConnectionCount];
|
||||
|
||||
List<byte[]> list = new(_connectionCount);
|
||||
List<WebClient> webClients = new(_connectionCount);
|
||||
List<byte[]> list = new(ConnectionCount);
|
||||
List<WebClient> webClients = new(ConnectionCount);
|
||||
|
||||
for (int i = 0; i < _connectionCount; i++)
|
||||
for (int i = 0; i < ConnectionCount; i++)
|
||||
{
|
||||
list.Add([]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _connectionCount; i++)
|
||||
for (int i = 0; i < ConnectionCount; i++)
|
||||
{
|
||||
#pragma warning disable SYSLIB0014
|
||||
// TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
|
||||
@@ -266,7 +258,7 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
webClients.Add(client);
|
||||
|
||||
if (i == _connectionCount - 1)
|
||||
if (i == ConnectionCount - 1)
|
||||
{
|
||||
client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}");
|
||||
}
|
||||
@@ -283,7 +275,7 @@ namespace Ryujinx.Ava.Systems
|
||||
Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
|
||||
Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
|
||||
|
||||
taskDialog.SetProgressBarState(totalProgressPercentage / _connectionCount, TaskDialogProgressState.Normal);
|
||||
taskDialog.SetProgressBarState(totalProgressPercentage / ConnectionCount, TaskDialogProgressState.Normal);
|
||||
};
|
||||
|
||||
client.DownloadDataCompleted += (_, args) =>
|
||||
@@ -302,10 +294,10 @@ namespace Ryujinx.Ava.Systems
|
||||
list[index] = args.Result;
|
||||
Interlocked.Increment(ref completedRequests);
|
||||
|
||||
if (Equals(completedRequests, _connectionCount))
|
||||
if (Equals(completedRequests, ConnectionCount))
|
||||
{
|
||||
byte[] mergedFileBytes = new byte[_buildSize];
|
||||
for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < _connectionCount; connectionIndex++)
|
||||
for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++)
|
||||
{
|
||||
Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length);
|
||||
destinationOffset += list[connectionIndex].Length;
|
||||
@@ -410,33 +402,73 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
private static void ExtractTarGzipFile(string archivePath, string outputDirectoryPath)
|
||||
private static void ExtractTarGzipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
|
||||
{
|
||||
using FileStream inStream = File.OpenRead(archivePath);
|
||||
using GZipStream gzipStream = new(inStream, CompressionMode.Decompress);
|
||||
|
||||
TarFile.ExtractToDirectory(gzipStream, outputDirectoryPath, true);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
private static void ExtractTarXzipFile(string archivePath, string outputDirectoryPath)
|
||||
{
|
||||
using FileStream inStream = File.OpenRead(archivePath);
|
||||
using XZStream gzipStream = new(inStream);
|
||||
|
||||
TarFile.ExtractToDirectory(gzipStream, outputDirectoryPath, true);
|
||||
using GZipInputStream gzipStream = new(inStream);
|
||||
using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
|
||||
|
||||
TarEntry tarEntry;
|
||||
|
||||
while ((tarEntry = tarStream.GetNextEntry()) is not null)
|
||||
{
|
||||
if (tarEntry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string outPath = Path.Combine(outputDirectoryPath, tarEntry.Name);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||
|
||||
using FileStream outStream = File.OpenWrite(outPath);
|
||||
tarStream.CopyEntryContents(outStream);
|
||||
|
||||
File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
|
||||
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (tarEntry is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
taskDialog.SetProgressBarState(GetPercentage(tarEntry.Size, inStream.Length), TaskDialogProgressState.Normal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractZipFile(string archivePath, string outputDirectoryPath)
|
||||
private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
|
||||
{
|
||||
ZipFile.ExtractToDirectory(archivePath, outputDirectoryPath);
|
||||
}
|
||||
|
||||
private static void Extract7ZipFile(string archivePath, string outputDirectoryPath)
|
||||
{
|
||||
IArchive archive = ArchiveFactory.OpenArchive(archivePath);
|
||||
archive.WriteToDirectory(outputDirectoryPath);
|
||||
using Stream inStream = File.OpenRead(archivePath);
|
||||
using ZipFile zipFile = new(inStream);
|
||||
|
||||
double count = 0;
|
||||
foreach (ZipEntry zipEntry in zipFile)
|
||||
{
|
||||
count++;
|
||||
if (zipEntry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string outPath = Path.Combine(outputDirectoryPath, zipEntry.Name);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||
|
||||
using Stream zipStream = zipFile.GetInputStream(zipEntry);
|
||||
using FileStream outStream = File.OpenWrite(outPath);
|
||||
|
||||
zipStream.CopyTo(outStream);
|
||||
|
||||
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void InstallUpdate(TaskDialog taskDialog, string updateFile)
|
||||
@@ -447,20 +479,16 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
ExtractTarXzipFile(updateFile, _updateDir);
|
||||
ExtractTarGzipFile(taskDialog, updateFile, _updateDir);
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
Extract7ZipFile(updateFile, _updateDir);
|
||||
ExtractZipFile(taskDialog, updateFile, _updateDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
// The new decompression implementations don't have a way to show progress
|
||||
// so the progressbar is just set to 100% after the decompression is done
|
||||
taskDialog.SetProgressBarState(100, TaskDialogProgressState.Normal);
|
||||
|
||||
// Delete downloaded zip
|
||||
File.Delete(updateFile);
|
||||
|
||||
@@ -16,41 +16,31 @@
|
||||
<Design.DataContext>
|
||||
<viewModels:ProfileSelectorDialogViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto">
|
||||
|
||||
<Border
|
||||
CornerRadius="5"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1">
|
||||
|
||||
<Border Padding="-3" BorderThickness="0">
|
||||
<ListBox
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
ItemsSource="{Binding Profiles}"
|
||||
SelectionChanged="ProfilesList_SelectionChanged">
|
||||
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel
|
||||
HorizontalAlignment="Left"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal" />
|
||||
Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Margin" Value="5 5 0 5" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="5" />
|
||||
</Style>
|
||||
<Style Selector="Rectangle#SelectionIndicator">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
|
||||
<ListBox.DataTemplates>
|
||||
<DataTemplate
|
||||
DataType="models:UserProfile">
|
||||
@@ -58,6 +48,7 @@
|
||||
PointerEntered="Grid_PointerEntered"
|
||||
PointerExited="Grid_OnPointerExited">
|
||||
<Border
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ClipToBounds="True"
|
||||
@@ -69,37 +60,26 @@
|
||||
<Image
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0,0,0,10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
MaxWidth="90"
|
||||
Text="{Binding Name}"
|
||||
Height="30"
|
||||
MaxWidth="90"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="2"
|
||||
Margin="5" />
|
||||
TextWrapping="Wrap"
|
||||
MaxLines="2" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<DataTemplate
|
||||
DataType="viewModels:BaseModel">
|
||||
<Panel
|
||||
Height="118"
|
||||
Width="96">
|
||||
<Panel.Styles>
|
||||
<Style Selector="Panel">
|
||||
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
|
||||
</Style>
|
||||
</Panel.Styles>
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle],
|
||||
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
Command="{Binding RunApplication}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale GameListContextMenuRunApplication}"
|
||||
Icon="{ext:Icon fa-solid fa-play}"/>
|
||||
Icon="{ext:Icon fa-solid fa-play}" />
|
||||
<MenuItem
|
||||
Command="{Binding ToggleFavorite}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale GameListContextMenuToggleFavorite}"
|
||||
Header="{Binding FavoriteStatusText}"
|
||||
Icon="{ext:Icon fa-solid fa-star}" />
|
||||
<MenuItem
|
||||
Command="{Binding CreateApplicationShortcut}"
|
||||
@@ -30,15 +30,15 @@
|
||||
ToolTip.Tip="{ext:Locale EditCustomConfigurationToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding EditGameConfiguration}"
|
||||
CommandParameter="{Binding}"
|
||||
IsVisible="{Binding !SelectedApplication.HasIndependentConfiguration}"
|
||||
Header="{ext:Locale GameListContextMenuCreateCustomConfiguration}"
|
||||
Icon="{ext:Icon fa-solid fa-gear}"
|
||||
ToolTip.Tip="{ext:Locale CreateCustomConfigurationToolTip}" />
|
||||
CommandParameter="{Binding}"
|
||||
IsVisible="{Binding !SelectedApplication.HasIndependentConfiguration}"
|
||||
Header="{ext:Locale GameListContextMenuCreateCustomConfiguration}"
|
||||
Icon="{ext:Icon fa-solid fa-gear}"
|
||||
ToolTip.Tip="{ext:Locale CreateCustomConfigurationToolTip}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding HasCompatibilityEntry}"
|
||||
Command="{Binding OpenApplicationCompatibility}"
|
||||
CommandParameter="{Binding}"
|
||||
IsVisible="{Binding HasCompatibilityEntry}"
|
||||
Header="{ext:Locale GameListContextMenuShowCompatEntry}"
|
||||
Icon="{ext:Icon fa-solid fa-database}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
|
||||
@@ -104,8 +104,8 @@
|
||||
Command="{Binding TrimXci}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale GameListContextMenuTrimXCI}"
|
||||
IsEnabled="{Binding TrimXCIEnabled}"
|
||||
Icon="{ext:Icon fa-solid fa-scissors}"
|
||||
IsEnabled="{Binding TrimXCIEnabled}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon fa-solid fa-memory}">
|
||||
<MenuItem
|
||||
@@ -151,9 +151,9 @@
|
||||
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding HasDlc}"
|
||||
Command="{Binding ExtractApplicationAocRomFs}"
|
||||
CommandParameter="{Binding}"
|
||||
IsVisible="{Binding HasDlc}"
|
||||
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />
|
||||
<MenuItem
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
NavigationDialogHost content = new(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle],
|
||||
PrimaryButtonText = string.Empty,
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = string.Empty,
|
||||
@@ -156,7 +156,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
_ = Dispatcher.UIThread.InvokeAsync(async ()
|
||||
=> await ContentDialogHelper.CreateErrorDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]));
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileDeletionWarningMessage]));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -165,8 +165,8 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
}
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage],
|
||||
string.Empty,
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileDeletionConfirmMessage],
|
||||
LocaleManager.Instance[LocaleKeys.IrreversibleActionNote],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
string.Empty);
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace Ryujinx.Ava.UI.Models
|
||||
[ObservableProperty]
|
||||
public partial byte[] Image { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool FirmwareFound { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Name { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.Core;
|
||||
using FluentAvalonia.UI.Windowing;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
@@ -54,9 +53,6 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
Name = FormatTitle();
|
||||
|
||||
// Disable menu animations
|
||||
FAUISettings.SetAnimationsEnabledAtAppLevel(false);
|
||||
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public partial class AboutWindowViewModel : BaseModel, IDisposable
|
||||
{
|
||||
[ObservableProperty] public partial Bitmap ForgejoLogo { get; set; }
|
||||
[ObservableProperty] public partial Bitmap GitLabLogo { get; set; }
|
||||
|
||||
[ObservableProperty] public partial Bitmap DiscordLogo { get; set; }
|
||||
|
||||
@@ -37,7 +37,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
|
||||
private const string LogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}_{1}.png?assembly=Ryujinx";
|
||||
private const string UnthemedLogoPathFormat = "resm:Ryujinx.Assets.UIImages.Logo_{0}.png?assembly=Ryujinx";
|
||||
|
||||
private void UpdateLogoTheme(string theme)
|
||||
{
|
||||
@@ -47,7 +46,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
string themeName = isDarkTheme ? "Dark" : "Light";
|
||||
|
||||
DiscordLogo = LoadBitmap(LogoPathFormat.Format("Discord", themeName));
|
||||
ForgejoLogo = LoadBitmap(UnthemedLogoPathFormat.Format("Forgejo"));
|
||||
GitLabLogo = LoadBitmap(LogoPathFormat.Format("GitLab", themeName));
|
||||
}
|
||||
|
||||
private static Bitmap LoadBitmap(string uri) => new(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
|
||||
@@ -56,7 +55,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
RyujinxApp.ThemeChanged -= Ryujinx_ThemeChanged;
|
||||
|
||||
ForgejoLogo.Dispose();
|
||||
GitLabLogo.Dispose();
|
||||
DiscordLogo.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
@@ -174,7 +174,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
private string _screenshotKey = "F8";
|
||||
private float _volume;
|
||||
private ApplicationData _currentApplicationData;
|
||||
private bool _pendingRestart;
|
||||
private readonly AutoResetEvent _rendererWaitEvent;
|
||||
private int _customVSyncInterval;
|
||||
private int _customVSyncIntervalPercentageProxy;
|
||||
@@ -371,39 +370,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
public bool CanScanAmiiboBinaries => AmiiboBinReader.HasAmiiboKeyFile;
|
||||
|
||||
public bool IsSkylanderRequested
|
||||
{
|
||||
get => field && _isGameRunning;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasSkylander
|
||||
{
|
||||
get => field && _isGameRunning;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSkylanderActions
|
||||
{
|
||||
get => field && _isGameRunning;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowLoadProgress
|
||||
{
|
||||
get;
|
||||
@@ -1063,7 +1029,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
string dialogMessage =
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogKeysInstallerKeysInstallMessage);
|
||||
|
||||
if (ContentManager.AreKeysAlreadyPresent(systemDirectory))
|
||||
if (ContentManager.AreKeysAlredyPresent(systemDirectory))
|
||||
{
|
||||
dialogMessage +=
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys
|
||||
@@ -1251,14 +1217,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
await LoadApplication(_currentApplicationData);
|
||||
}
|
||||
else if (_pendingRestart)
|
||||
{
|
||||
_pendingRestart = false;
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"Restarting emulation for '{_currentApplicationData.Name}'");
|
||||
|
||||
await LoadApplication(_currentApplicationData);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, clear state.
|
||||
@@ -1267,21 +1225,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public void RestartEmulation()
|
||||
{
|
||||
if (AppHost is null || _currentApplicationData is null)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, "RestartEmulation called but no application is running.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"Restart requested for '{_currentApplicationData.Name}'");
|
||||
|
||||
_pendingRestart = true;
|
||||
AppHost.Stop();
|
||||
}
|
||||
|
||||
private void Update_StatusBar(object sender, StatusUpdatedEventArgs args)
|
||||
{
|
||||
if (ShowMenuAndStatusBar && !ShowLoadProgress)
|
||||
@@ -1921,46 +1864,6 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
}
|
||||
public async Task OpenSkylanderWindow()
|
||||
{
|
||||
if (AppHost.Device.System.SearchingForSkylander(out int deviceId))
|
||||
{
|
||||
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(
|
||||
new FilePickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.sky", "*.bin", "*.dmp", "*.dump"],
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.HasValue)
|
||||
{
|
||||
// Open reading stream from the first file.
|
||||
await using var stream = await result.Value.OpenReadAsync();
|
||||
using var streamReader = new BinaryReader(stream);
|
||||
// Reads all the content of file as a text.
|
||||
byte[] data = new byte[1024];
|
||||
var count = streamReader.Read(data, 0, 1024);
|
||||
if (count < 1024)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
AppHost.Device.System.ScanSkylander(deviceId, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveSkylander()
|
||||
{
|
||||
AppHost.Device.System.RemoveSkylander();
|
||||
}
|
||||
|
||||
public void ReloadRenderDocApi()
|
||||
{
|
||||
@@ -2214,6 +2117,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
);
|
||||
|
||||
public string FavoriteStatusText => SelectedApplication?.Favorite == false ? LocaleManager.Instance[LocaleKeys.GameListContextMenuAddToFavorites] : LocaleManager.Instance[LocaleKeys.GameListContextMenuRemoveFromFavorites];
|
||||
|
||||
public static RelayCommand<MainWindowViewModel> CreateApplicationShortcut { get; } =
|
||||
Commands.CreateConditional<MainWindowViewModel>(vm => vm?.SelectedApplication != null,
|
||||
viewModel => ShortcutHelper.CreateAppShortcut(
|
||||
|
||||
@@ -28,6 +28,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ProfileImageModel> Images { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool FirmwareFound { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color BackgroundColor { get; set; } = Colors.White;
|
||||
|
||||
@@ -43,17 +46,15 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
};
|
||||
}
|
||||
|
||||
private int _selectedIndex = -1;
|
||||
|
||||
public int SelectedIndex
|
||||
{
|
||||
get;
|
||||
get => _selectedIndex;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
|
||||
SelectedImage = field == -1
|
||||
? null
|
||||
: Images[field].Data;
|
||||
|
||||
_selectedIndex = value;
|
||||
SelectedImage = value == -1 ? null : Images[value].Data;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(SelectedImage));
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public partial class UserProfileImageSelectorViewModel : BaseModel
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool FirmwareFound { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
@@ -9,20 +9,35 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public UserProfileViewModel()
|
||||
{
|
||||
Profiles = [];
|
||||
LostProfiles = [];
|
||||
IsEmpty = !LostProfiles.Any();
|
||||
Profiles = new ObservableCollection<BaseModel>();
|
||||
LostProfiles = new ObservableCollection<UserProfile>();
|
||||
LostProfiles.CollectionChanged += LostProfilesChanged;
|
||||
}
|
||||
|
||||
public ObservableCollection<BaseModel> Profiles { get; set; }
|
||||
public ObservableCollection<BaseModel> Profiles { get; }
|
||||
|
||||
public ObservableCollection<UserProfile> LostProfiles { get; set; }
|
||||
public ObservableCollection<UserProfile> LostProfiles { get; }
|
||||
|
||||
public bool IsEmpty { get; set; }
|
||||
public bool IsEmpty => LostProfiles.Count == 0;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
LostProfiles.CollectionChanged -= LostProfilesChanged;
|
||||
}
|
||||
|
||||
private void LostProfilesChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsEmpty));
|
||||
}
|
||||
|
||||
public void UpdateLostProfiles(ObservableCollection<UserProfile> newProfiles)
|
||||
{
|
||||
LostProfiles.Clear();
|
||||
|
||||
foreach (var profile in newProfiles)
|
||||
LostProfiles.Add(profile);
|
||||
|
||||
OnPropertyChanged(nameof(IsEmpty));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private readonly AccountManager _accountManager;
|
||||
|
||||
public string SaveManagerHeading => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SaveManagerHeading, _accountManager.LastOpenedUser.Name, _accountManager.LastOpenedUser.UserId);
|
||||
public string SaveManagerTitle => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UserProfiles_SaveManagerTitle, _accountManager.LastOpenedUser.Name);
|
||||
|
||||
public UserSaveManagerViewModel(AccountManager accountManager)
|
||||
{
|
||||
|
||||
@@ -122,8 +122,8 @@
|
||||
Click="Button_OnClick"
|
||||
CornerRadius="15"
|
||||
Tag="https://src.ryujinx.app"
|
||||
ToolTip.Tip="{ext:Locale AboutForgejoUrlTooltipMessage}">
|
||||
<Image Source="{Binding ForgejoLogo}" />
|
||||
ToolTip.Tip="{ext:Locale AboutGitLabUrlTooltipMessage}">
|
||||
<Image Source="{Binding GitLabLogo}" />
|
||||
</Button>
|
||||
<Button
|
||||
MinWidth="30"
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.Views.Dialog
|
||||
{
|
||||
PrimaryButtonText = string.Empty,
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
Content = new AboutView { ViewModel = viewModel }
|
||||
};
|
||||
|
||||
|
||||
@@ -47,13 +47,18 @@
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" IsEnabled="{Binding ShowLedColorPicker}">
|
||||
<TextBlock MinWidth="75" MaxWidth="200" Text="{ext:Locale ControllerSettingsLedColor}" />
|
||||
<ColorPicker
|
||||
<ui:ColorPickerButton
|
||||
Margin="5"
|
||||
IsMoreButtonVisible="False"
|
||||
UseColorPalette="False"
|
||||
UseColorTriangle="False"
|
||||
UseColorWheel="False"
|
||||
ShowAcceptDismissButtons="False"
|
||||
IsAlphaEnabled="False"
|
||||
AttachedToVisualTree="ColorPicker_OnAttachedToVisualTree"
|
||||
ColorChanged="ColorPicker_OnColorChanged"
|
||||
AttachedToVisualTree="ColorPickerButton_OnAttachedToVisualTree"
|
||||
ColorChanged="ColorPickerButton_OnColorChanged"
|
||||
Color="{Binding LedColor, Mode=TwoWay}">
|
||||
</ColorPicker>
|
||||
</ui:ColorPickerButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
@@ -31,17 +30,19 @@ namespace Ryujinx.UI.Views.Input
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void ColorPicker_OnColorChanged(object sender, ColorChangedEventArgs args)
|
||||
private void ColorPickerButton_OnColorChanged(ColorPickerButton sender, ColorButtonColorChangedEventArgs args)
|
||||
{
|
||||
if (!args.NewColor.HasValue)
|
||||
return;
|
||||
if (!ViewModel.EnableLedChanging)
|
||||
return;
|
||||
if (ViewModel.TurnOffLed)
|
||||
return;
|
||||
|
||||
ViewModel.ParentModel.SelectedGamepad.SetLed(args.NewColor.ToUInt32());
|
||||
ViewModel.ParentModel.SelectedGamepad.SetLed(args.NewColor.Value.ToUInt32());
|
||||
}
|
||||
|
||||
private void ColorPicker_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||
private void ColorPickerButton_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (!ViewModel.EnableLedChanging)
|
||||
return;
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
<MenuItem
|
||||
Command="{Binding ManageProfiles}"
|
||||
Padding="0"
|
||||
Header="{ext:Locale MenuBarOptionsManageUserProfiles}"
|
||||
Header="{ext:Locale UserProfiles_MenuBarOptions_OpenUserProfiles}"
|
||||
Icon="{ext:Icon fa-solid fa-user}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
Classes="withCheckbox">
|
||||
@@ -167,12 +167,6 @@
|
||||
Icon="{ext:Icon fa-solid fa-stop}"
|
||||
InputGesture="Escape"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
Name="RestartEmulationMenuItem"
|
||||
Header="{ext:Locale MenuBarOptionsRestartEmulation}"
|
||||
Icon="{ext:Icon fa-solid fa-rotate-right}"
|
||||
InputGesture="Ctrl + R"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{ext:Locale MenuBarOptionsSimulateWakeUpMessage}" Icon="{ext:Icon fa-solid fa-sun}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
@@ -190,22 +184,6 @@
|
||||
IsVisible="{Binding CanScanAmiiboBinaries}"
|
||||
InputGesture="Ctrl + B"
|
||||
IsEnabled="{Binding IsAmiiboBinRequested}" />
|
||||
<MenuItem
|
||||
Command="{Binding OpenSkylanderWindow}"
|
||||
AttachedToVisualTree="ScanSkylanderMenuItem_AttachedToVisualTree"
|
||||
Header="{ext:Locale MenuBarActionsScanSkylander}"
|
||||
Icon="{ext:Icon fa-solid fa-cube}"
|
||||
IsVisible="{Binding ShowSkylanderActions}"
|
||||
InputGesture="Ctrl + S"
|
||||
IsEnabled="{Binding IsSkylanderRequested}" />
|
||||
<MenuItem
|
||||
Command="{Binding RemoveSkylander}"
|
||||
AttachedToVisualTree="RemoveSkylanderMenuItem_AttachedToVisualTree"
|
||||
Header="{ext:Locale MenuBarActionsRemoveSkylander}"
|
||||
Icon="{ext:Icon fa-solid fa-cube}"
|
||||
IsVisible="{Binding ShowSkylanderActions}"
|
||||
InputGesture="Ctrl + D"
|
||||
IsEnabled="{Binding HasSkylander}" />
|
||||
<MenuItem
|
||||
Command="{Binding TakeScreenshot}"
|
||||
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"
|
||||
@@ -295,17 +273,17 @@
|
||||
<MenuItem
|
||||
Name="SetupGuideMenuItem"
|
||||
Header="{ext:Locale MenuBarHelpSetup}"
|
||||
Icon="{ext:Icon fa-solid fa-circle-info}"
|
||||
Icon="{ext:Icon fa-brands fa-gitlab}"
|
||||
CommandParameter="{x:Static common:SharedConstants.SetupGuideWikiUrl}" />
|
||||
<MenuItem
|
||||
Name="LdnGuideMenuItem"
|
||||
Header="{ext:Locale MenuBarHelpMultiplayer}"
|
||||
Icon="{ext:Icon fa-solid fa-circle-info}"
|
||||
Icon="{ext:Icon fa-brands fa-gitlab}"
|
||||
CommandParameter="{x:Static common:SharedConstants.MultiplayerWikiUrl}" />
|
||||
<MenuItem
|
||||
Name="FaqMenuItem"
|
||||
Header="{ext:Locale MenuBarHelpFaq}"
|
||||
Icon="{ext:Icon fa-solid fa-circle-info}"
|
||||
Icon="{ext:Icon fa-brands fa-gitlab}"
|
||||
CommandParameter="{x:Static common:SharedConstants.FaqWikiUrl}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
|
||||
@@ -43,7 +43,6 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
|
||||
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
|
||||
StopEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
|
||||
RestartEmulationMenuItem.Command = Commands.Create(() => ViewModel.RestartEmulation());
|
||||
CheatManagerMenuItem.Command = Commands.CreateSilentFail(OpenCheatManagerForCurrentApp);
|
||||
InstallFileTypesMenuItem.Command = Commands.Create(InstallFileTypes);
|
||||
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
|
||||
@@ -194,20 +193,6 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
ViewModel.IsAmiiboBinRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasAmiiboKeyFile;
|
||||
}
|
||||
|
||||
private void ScanSkylanderMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem)
|
||||
ViewModel.IsSkylanderRequested = ViewModel.AppHost.Device.System.SearchingForSkylander(out _);
|
||||
ViewModel.ShowSkylanderActions = string.Equals(ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(), "0100CCC0002E6000");
|
||||
}
|
||||
|
||||
private void RemoveSkylanderMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem)
|
||||
ViewModel.HasSkylander = ViewModel.AppHost.Device.System.HasSkylander(out _);
|
||||
ViewModel.ShowSkylanderActions = string.Equals(ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(), "0100CCC0002E6000");
|
||||
}
|
||||
|
||||
private async Task InstallFileTypes()
|
||||
{
|
||||
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();
|
||||
|
||||
@@ -8,107 +8,102 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||
Margin="0"
|
||||
MinWidth="500"
|
||||
Padding="0"
|
||||
mc:Ignorable="d"
|
||||
MinWidth="500"
|
||||
Focusable="True"
|
||||
x:DataType="models:TempProfile">
|
||||
<Grid Margin="0" ColumnDefinitions="Auto,*" RowDefinitions="*,Auto">
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Text="{ext:Locale UserProfilesName}" />
|
||||
<Grid Margin="0,10,0,0" ColumnDefinitions="Auto,*" RowDefinitions="*,Auto">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_NameLabel}" />
|
||||
<TextBox
|
||||
Name="NameBox"
|
||||
Margin="0,0,0,5"
|
||||
Width="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
MaxLength="{Binding MaxProfileNameLength}"
|
||||
Watermark="{ext:Locale ProfileNameSelectionWatermark}"
|
||||
Watermark="{ext:Locale UserProfiles_ProfileNameSelectionWatermark}"
|
||||
Text="{Binding Name}" />
|
||||
<TextBlock Name="IdText" Text="{ext:Locale UserProfilesUserId}" />
|
||||
<TextBlock Name="IdText" Text="{ext:Locale UserProfiles_UserIdLabel}" />
|
||||
<TextBox
|
||||
Name="IdLabel"
|
||||
Width="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding UserIdString}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<Grid Grid.Column="1">
|
||||
<Border
|
||||
Name="ImageBox"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1">
|
||||
BorderThickness="1"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Right">
|
||||
<Panel>
|
||||
<ui:SymbolIcon
|
||||
FontSize="60"
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0"
|
||||
FontSize="70"
|
||||
Width="120"
|
||||
Height="120"
|
||||
Foreground="{DynamicResource AppListHoverBackgroundColor}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Symbol="Camera" />
|
||||
<Image
|
||||
Name="ProfileImage"
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Width="120"
|
||||
Height="120"
|
||||
Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
<Border
|
||||
Margin="2"
|
||||
Height="27"
|
||||
Width="27"
|
||||
CornerRadius="17"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}">
|
||||
<Button
|
||||
Name="ProfileImageButton"
|
||||
MaxHeight="27"
|
||||
MaxWidth="27"
|
||||
MinWidth="27"
|
||||
MinHeight="27"
|
||||
CornerRadius="17"
|
||||
Padding="0">
|
||||
<ui:SymbolIcon Symbol="Edit" />
|
||||
<Button.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuItem
|
||||
Header="{ext:Locale UserProfiles_ProfileImage_Import}"
|
||||
Icon="{ext:Icon fa-solid fa-image}"
|
||||
Click="Import_OnClick" />
|
||||
<MenuItem
|
||||
Header="{ext:Locale UserProfiles_ProfileImage_SelectAvatar}"
|
||||
Icon="{ext:Icon fa-solid fa-floppy-disk}"
|
||||
Click="SelectFirmwareImage_OnClick" />
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</Border>
|
||||
</Panel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
Spacing="10">
|
||||
<Button
|
||||
Width="50"
|
||||
MinWidth="50"
|
||||
Click="BackButton_Click">
|
||||
<Button MinWidth="50" Width="50" Click="BackButton_Click">
|
||||
<ui:SymbolIcon Symbol="Back" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
Spacing="10">
|
||||
<Button
|
||||
Name="DeleteButton"
|
||||
Click="DeleteButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfilesDelete}" />
|
||||
<Button Name="DeleteButton" Click="DeleteButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_ButtonDelete}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="ChangePictureButton"
|
||||
Click="ChangePictureButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfilesChangeProfileImage}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="AddPictureButton"
|
||||
Click="ChangePictureButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfilesSetProfileImage}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
Click="SaveButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfilesSave}" />
|
||||
<Button Name="SaveButton" Click="SaveButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_ButtonSave}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml.MarkupExtensions;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.VisualTree;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
@@ -8,6 +12,10 @@ using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SkiaSharp;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.User
|
||||
@@ -15,90 +23,75 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
public partial class UserEditorView : RyujinxControl<TempProfile>
|
||||
{
|
||||
private NavigationDialogHost _parent;
|
||||
private ContentManager _contentManager;
|
||||
private UserProfile _profile;
|
||||
private TempProfile _tempProfile;
|
||||
private bool _isNewUser;
|
||||
|
||||
public static uint MaxProfileNameLength => 0x20;
|
||||
public bool IsDeletable => _profile.UserId != AccountManager.DefaultUserId;
|
||||
|
||||
public string UserEditorTitle => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UserProfiles_UserEditorTitle, _profile.Name);
|
||||
public UserEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
if (!Program.PreviewerDetached)
|
||||
return;
|
||||
|
||||
if (arg.NavigationMode == NavigationMode.New)
|
||||
{
|
||||
switch (arg.NavigationMode)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
(NavigationDialogHost parent, UserProfile profile, bool isNewUser) = ((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
|
||||
_isNewUser = isNewUser;
|
||||
_profile = profile;
|
||||
ViewModel = new TempProfile(_profile);
|
||||
(NavigationDialogHost parent, UserProfile profile, bool isNewUser) =
|
||||
((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
|
||||
|
||||
_parent = parent;
|
||||
break;
|
||||
}
|
||||
_parent = parent;
|
||||
_profile = profile;
|
||||
_isNewUser = isNewUser;
|
||||
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - " +
|
||||
$"{(_isNewUser ? LocaleManager.Instance[LocaleKeys.UserEditorTitleCreate] : LocaleManager.Instance[LocaleKeys.UserEditorTitle])}";
|
||||
DataValidationErrors.ClearErrors(NameBox);
|
||||
DataValidationErrors.ClearErrors(ImageBox);
|
||||
ImageBox.Bind(Border.BorderBrushProperty, new DynamicResourceExtension("AppListHoverBackgroundColor"));
|
||||
|
||||
AddPictureButton.IsVisible = _isNewUser;
|
||||
ChangePictureButton.IsVisible = !_isNewUser;
|
||||
IdLabel.IsVisible = _profile != null;
|
||||
IdText.IsVisible = _profile != null;
|
||||
if (!_isNewUser && IsDeletable)
|
||||
{
|
||||
DeleteButton.IsVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DeleteButton.IsVisible = false;
|
||||
}
|
||||
ViewModel = new TempProfile(_profile);
|
||||
_tempProfile = ViewModel;
|
||||
|
||||
_contentManager = _parent.ContentManager;
|
||||
ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null;
|
||||
}
|
||||
|
||||
((ContentDialog)_parent.Parent).Title =
|
||||
$"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - " +
|
||||
$"{(_isNewUser ? LocaleManager.Instance[LocaleKeys.UserProfiles_UserEditorTitleNewUser] : UserEditorTitle)}";
|
||||
|
||||
bool hasProfile = _profile != null;
|
||||
IdLabel.IsVisible = hasProfile;
|
||||
IdText.IsVisible = hasProfile;
|
||||
|
||||
DeleteButton.IsVisible = !_isNewUser && IsDeletable;
|
||||
}
|
||||
|
||||
private async void BackButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isNewUser)
|
||||
bool hasUnsavedChanges =
|
||||
_isNewUser
|
||||
? (ViewModel.Name != string.Empty || ViewModel.Image != null)
|
||||
: (_profile.Name != ViewModel.Name || _profile.Image != ViewModel.Image);
|
||||
|
||||
if (hasUnsavedChanges)
|
||||
{
|
||||
if (ViewModel.Name != string.Empty || ViewModel.Image != null)
|
||||
{
|
||||
if (await ContentDialogHelper.CreateChoiceDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
|
||||
{
|
||||
_parent?.GoBack();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bool confirm = await ContentDialogHelper.CreateChoiceDialog(
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesTitle],
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesMessage],
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesSubMessage]);
|
||||
|
||||
if (confirm)
|
||||
_parent?.GoBack();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_profile.Name != ViewModel.Name || _profile.Image != ViewModel.Image)
|
||||
{
|
||||
if (await ContentDialogHelper.CreateChoiceDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
|
||||
{
|
||||
_parent?.GoBack();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_parent?.GoBack();
|
||||
}
|
||||
_parent?.GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,18 +103,28 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DataValidationErrors.ClearErrors(NameBox);
|
||||
DataValidationErrors.ClearErrors(ImageBox);
|
||||
ImageBox.Bind(Border.BorderBrushProperty, new DynamicResourceExtension("AppListHoverBackgroundColor"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ViewModel.Name))
|
||||
bool nameEmpty = string.IsNullOrWhiteSpace(ViewModel.Name);
|
||||
bool imageMissing = ViewModel.Image == null;
|
||||
|
||||
if (nameEmpty && imageMissing)
|
||||
{
|
||||
DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfiles_EmptyNameError]));
|
||||
DataValidationErrors.SetError(ImageBox, new DataValidationException(""));
|
||||
ImageBox.BorderBrush = Brush.Parse("#ff99a4");
|
||||
return;
|
||||
}
|
||||
else if (nameEmpty)
|
||||
{
|
||||
DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfileEmptyNameError]));
|
||||
|
||||
DataValidationErrors.SetError(NameBox,new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfiles_EmptyNameError]));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ViewModel.Image == null)
|
||||
else if (imageMissing)
|
||||
{
|
||||
_parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, ViewModel));
|
||||
|
||||
DataValidationErrors.SetError(ImageBox, new DataValidationException(""));
|
||||
ImageBox.BorderBrush = Brush.Parse("#ff99a4");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,6 +133,7 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
_profile.Name = ViewModel.Name;
|
||||
_profile.Image = ViewModel.Image;
|
||||
_profile.UpdateState();
|
||||
|
||||
_parent.AccountManager.SetUserName(_profile.UserId, _profile.Name);
|
||||
_parent.AccountManager.SetUserImage(_profile.UserId, _profile.Image);
|
||||
}
|
||||
@@ -145,17 +149,80 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
_parent?.GoBack();
|
||||
}
|
||||
|
||||
public void SelectProfileImage()
|
||||
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, ViewModel));
|
||||
}
|
||||
|
||||
private void ChangePictureButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_profile != null || _isNewUser)
|
||||
if (ViewModel.FirmwareFound)
|
||||
{
|
||||
SelectProfileImage();
|
||||
_parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _tempProfile));
|
||||
}
|
||||
}
|
||||
|
||||
private async void Import_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var window = (Window)this.GetVisualRoot()!;
|
||||
var result = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_SupportedImageFormatDialogTitle],
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp"],
|
||||
AppleUniformTypeIdentifiers = ["public.jpeg", "public.png", "com.microsoft.bmp"],
|
||||
MimeTypes = ["image/jpeg", "image/png", "image/bmp"],
|
||||
},
|
||||
new("JPG")
|
||||
{
|
||||
Patterns = ["*.jpg"],
|
||||
AppleUniformTypeIdentifiers = ["public.jpeg"],
|
||||
MimeTypes = ["image/jpeg"],
|
||||
},
|
||||
new("JPEG")
|
||||
{
|
||||
Patterns = ["*.jpeg"],
|
||||
AppleUniformTypeIdentifiers = ["public.jpeg"],
|
||||
MimeTypes = ["image/jpeg"],
|
||||
},
|
||||
new("PNG")
|
||||
{
|
||||
Patterns = ["*.png"],
|
||||
AppleUniformTypeIdentifiers = ["public.png"],
|
||||
MimeTypes = ["image/png"],
|
||||
},
|
||||
new("BMP")
|
||||
{
|
||||
Patterns = ["*.bmp"],
|
||||
AppleUniformTypeIdentifiers = ["com.microsoft.bmp"],
|
||||
MimeTypes = ["image/bmp"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result.Count == 0 || DataContext is not TempProfile temp)
|
||||
return;
|
||||
|
||||
temp.Image = ProcessProfileImage(File.ReadAllBytes(result[0].Path.LocalPath));
|
||||
|
||||
if (_profile != null)
|
||||
_profile.Image = temp.Image;
|
||||
}
|
||||
|
||||
private static byte[] ProcessProfileImage(byte[] buffer)
|
||||
{
|
||||
using SKBitmap bitmap = SKBitmap.Decode(buffer);
|
||||
SKBitmap resizedBitmap = bitmap.Resize(new SKImageInfo(256, 256), SKFilterQuality.High);
|
||||
|
||||
using MemoryStream streamJpg = new();
|
||||
|
||||
if (resizedBitmap != null)
|
||||
{
|
||||
using SKImage image = SKImage.FromBitmap(resizedBitmap);
|
||||
using SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100);
|
||||
dataJpeg.SaveTo(streamJpg);
|
||||
}
|
||||
|
||||
return streamJpg.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1,81 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
d:DesignHeight="350"
|
||||
d:DesignWidth="578"
|
||||
mc:Ignorable="d"
|
||||
Width="528"
|
||||
d:DesignWidth="578"
|
||||
d:DesignHeight="350"
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel"
|
||||
Focusable="True">
|
||||
Focusable="True"
|
||||
x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:UserFirmwareAvatarSelectorViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto,Auto">
|
||||
<ListBox
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto,Auto">
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
BorderThickness="0"
|
||||
SelectedIndex="{Binding SelectedIndex}"
|
||||
Height="400"
|
||||
ItemsSource="{Binding Images}"
|
||||
Padding="2.5"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
CornerRadius="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel
|
||||
Orientation="Horizontal"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Width" Value="85" />
|
||||
<Setter Property="MaxWidth" Value="85" />
|
||||
<Setter Property="MinWidth" Value="85" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem /template/ Rectangle#SelectionIndicator">
|
||||
<Setter Property="MinHeight" Value="70" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Panel
|
||||
Background="{Binding BackgroundColor}"
|
||||
Margin="5">
|
||||
<Image Source="{Binding Data, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
VerticalAlignment="Stretch">
|
||||
<ListBox
|
||||
Grid.Row="1"
|
||||
Background="Transparent"
|
||||
SelectedIndex="{Binding SelectedIndex}"
|
||||
Height="400"
|
||||
ItemsSource="{Binding Images}">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel
|
||||
Orientation="Horizontal"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Width" Value="85" />
|
||||
<Setter Property="MaxWidth" Value="85" />
|
||||
<Setter Property="MinWidth" Value="85" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem /template/ Rectangle#SelectionIndicator">
|
||||
<Setter Property="MinHeight" Value="70" />
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Panel
|
||||
Background="{Binding BackgroundColor}"
|
||||
Margin="5">
|
||||
<Image Source="{Binding Data, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
<StackPanel
|
||||
Grid.Row="3"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
HorizontalAlignment="Left">
|
||||
<Button
|
||||
Width="50"
|
||||
MinWidth="50"
|
||||
Height="35"
|
||||
Height="37"
|
||||
Click="GoBack">
|
||||
<ui:SymbolIcon Symbol="Back" />
|
||||
</Button>
|
||||
@@ -76,23 +84,29 @@
|
||||
Grid.Row="3"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
HorizontalAlignment="Right">
|
||||
<ColorPicker
|
||||
<ui:ColorPickerButton
|
||||
FlyoutPlacement="Top"
|
||||
IsMoreButtonVisible="False"
|
||||
UseColorPalette="False"
|
||||
UseColorTriangle="False"
|
||||
UseColorWheel="False"
|
||||
ShowAcceptDismissButtons="False"
|
||||
IsAlphaEnabled="False"
|
||||
Color="{Binding BackgroundColor, Mode=TwoWay}"
|
||||
Name="ColorButton">
|
||||
<ColorPicker.Styles>
|
||||
<ui:ColorPickerButton.Styles>
|
||||
<Style Selector="Grid#Root > DockPanel > Grid">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</ColorPicker.Styles>
|
||||
</ColorPicker>
|
||||
</ui:ColorPickerButton.Styles>
|
||||
</ui:ColorPickerButton>
|
||||
<Button
|
||||
Content="{ext:Locale AvatarChoose}"
|
||||
Height="35"
|
||||
Name="ChooseButton"
|
||||
Click="ChooseButton_OnClick" />
|
||||
Height="37"
|
||||
Click="ChooseButton_OnClick">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_ButtonChooseAvatar}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SkiaSharp;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.User
|
||||
{
|
||||
@@ -14,44 +17,53 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
{
|
||||
private NavigationDialogHost _parent;
|
||||
private TempProfile _profile;
|
||||
public ContentManager ContentManager { get; private set; }
|
||||
public UserFirmwareAvatarSelectorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
ViewModel = new UserFirmwareAvatarSelectorViewModel();
|
||||
DataContext = ViewModel;
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
public UserFirmwareAvatarSelectorView(ContentManager contentManager)
|
||||
{
|
||||
ContentManager = contentManager;
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public UserFirmwareAvatarSelectorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
ViewModel = new UserFirmwareAvatarSelectorViewModel();
|
||||
DataContext = ViewModel;
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
if (!Program.PreviewerDetached)
|
||||
return;
|
||||
|
||||
if (arg.NavigationMode != NavigationMode.New)
|
||||
return;
|
||||
|
||||
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||
ContentManager = _parent.ContentManager;
|
||||
|
||||
((ContentDialog)_parent.Parent).Title =
|
||||
$"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - " +
|
||||
$"{LocaleManager.Instance[LocaleKeys.UserProfiles_SelectAvatarTitle]}";
|
||||
|
||||
ViewModel.SelectedIndex = -1;
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
if (arg.NavigationMode == NavigationMode.New)
|
||||
bool found = ContentManager.GetCurrentFirmwareVersion() != null;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||
ContentManager = _parent.ContentManager;
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
ViewModel = new UserFirmwareAvatarSelectorViewModel();
|
||||
}
|
||||
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
}
|
||||
ViewModel.FirmwareFound = found;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public ContentManager ContentManager { get; private set; }
|
||||
|
||||
private void GoBack(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_parent.GoBack();
|
||||
@@ -59,32 +71,31 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
|
||||
private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel.SelectedImage != null)
|
||||
if (ViewModel.SelectedImage == null)
|
||||
return;
|
||||
|
||||
using MemoryStream streamJpg = new();
|
||||
using SKBitmap bitmap = SKBitmap.Decode(ViewModel.SelectedImage);
|
||||
using SKBitmap newBitmap = new(bitmap.Width, bitmap.Height);
|
||||
|
||||
using (SKCanvas canvas = new(newBitmap))
|
||||
{
|
||||
using MemoryStream streamJpg = new();
|
||||
using SKBitmap bitmap = SKBitmap.Decode(ViewModel.SelectedImage);
|
||||
using SKBitmap newBitmap = new(bitmap.Width, bitmap.Height);
|
||||
|
||||
using (SKCanvas canvas = new(newBitmap))
|
||||
{
|
||||
canvas.Clear(new SKColor(
|
||||
ViewModel.BackgroundColor.R,
|
||||
ViewModel.BackgroundColor.G,
|
||||
ViewModel.BackgroundColor.B,
|
||||
ViewModel.BackgroundColor.A));
|
||||
canvas.DrawBitmap(bitmap, 0, 0);
|
||||
}
|
||||
|
||||
using (SKImage image = SKImage.FromBitmap(newBitmap))
|
||||
using (SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100))
|
||||
{
|
||||
dataJpeg.SaveTo(streamJpg);
|
||||
}
|
||||
|
||||
_profile.Image = streamJpg.ToArray();
|
||||
|
||||
_parent.GoBack();
|
||||
canvas.Clear(new SKColor(
|
||||
ViewModel.BackgroundColor.R,
|
||||
ViewModel.BackgroundColor.G,
|
||||
ViewModel.BackgroundColor.B,
|
||||
ViewModel.BackgroundColor.A));
|
||||
canvas.DrawBitmap(bitmap, 0, 0);
|
||||
}
|
||||
|
||||
using (SKImage image = SKImage.FromBitmap(newBitmap))
|
||||
using (SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100))
|
||||
{
|
||||
dataJpeg.SaveTo(streamJpg);
|
||||
}
|
||||
|
||||
_profile.Image = streamJpg.ToArray();
|
||||
_parent.GoBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:viewModles="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
Focusable="True"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserProfileImageSelectorView"
|
||||
x:DataType="viewModles:UserProfileImageSelectorViewModel"
|
||||
Width="500"
|
||||
d:DesignWidth="500">
|
||||
<Design.DataContext>
|
||||
<viewModles:UserProfileImageSelectorViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center" RowDefinitions="Auto,70,Auto">
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Left"
|
||||
TextAlignment="Start"
|
||||
Text="{ext:Locale ProfileImageSelectionNote}" />
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="50"
|
||||
MinWidth="50"
|
||||
Click="GoBack">
|
||||
<ui:SymbolIcon Symbol="Back" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Name="Import"
|
||||
Click="Import_OnClick">
|
||||
<TextBlock Text="{ext:Locale ProfileImageSelectionImportImage}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="SelectFirmwareImage"
|
||||
IsEnabled="{Binding FirmwareFound}"
|
||||
Click="SelectFirmwareImage_OnClick">
|
||||
<TextBlock Text="{ext:Locale ProfileImageSelectionSelectAvatar}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,118 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.VisualTree;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SkiaSharp;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.User
|
||||
{
|
||||
public partial class UserProfileImageSelectorView : RyujinxControl<UserProfileImageSelectorViewModel>
|
||||
{
|
||||
private ContentManager _contentManager;
|
||||
private NavigationDialogHost _parent;
|
||||
private TempProfile _profile;
|
||||
|
||||
public UserProfileImageSelectorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
switch (arg.NavigationMode)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||
_contentManager = _parent.ContentManager;
|
||||
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.ProfileImageSelectionHeader]}";
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
DataContext = ViewModel = new UserProfileImageSelectorViewModel();
|
||||
ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null;
|
||||
}
|
||||
|
||||
break;
|
||||
case NavigationMode.Back:
|
||||
if (_profile.Image != null)
|
||||
{
|
||||
_parent.GoBack();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void Import_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
IReadOnlyList<IStorageFile> result = await ((Window)this.GetVisualRoot()!).StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp"],
|
||||
AppleUniformTypeIdentifiers = ["public.jpeg", "public.png", "com.microsoft.bmp"],
|
||||
MimeTypes = ["image/jpeg", "image/png", "image/bmp"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result.Count > 0)
|
||||
{
|
||||
_profile.Image = ProcessProfileImage(File.ReadAllBytes(result[0].Path.LocalPath));
|
||||
_parent.GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_parent.GoBack();
|
||||
}
|
||||
|
||||
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel.FirmwareFound)
|
||||
{
|
||||
_parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _profile));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ProcessProfileImage(byte[] buffer)
|
||||
{
|
||||
using SKBitmap bitmap = SKBitmap.Decode(buffer);
|
||||
|
||||
SKBitmap resizedBitmap = bitmap.Resize(new SKImageInfo(256, 256), SKFilterQuality.High);
|
||||
|
||||
using MemoryStream streamJpg = new();
|
||||
|
||||
if (resizedBitmap != null)
|
||||
{
|
||||
using SKImage image = SKImage.FromBitmap(resizedBitmap);
|
||||
using SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100);
|
||||
|
||||
dataJpeg.SaveTo(streamJpg);
|
||||
}
|
||||
|
||||
return streamJpg.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,64 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
d:DesignWidth="550"
|
||||
d:DesignHeight="450"
|
||||
mc:Ignorable="d"
|
||||
Width="500"
|
||||
Height="400"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView"
|
||||
x:DataType="viewModels:UserProfileViewModel"
|
||||
Focusable="True">
|
||||
Focusable="True"
|
||||
x:DataType="viewModels:UserProfileViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:UserProfileViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" RowDefinitions="*,Auto">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto">
|
||||
<Border
|
||||
CornerRadius="5"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1"
|
||||
Grid.Row="0">
|
||||
Grid.Row="0"
|
||||
Padding="2.5">
|
||||
<Panel>
|
||||
<ListBox
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{Binding LostProfiles}">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5">
|
||||
CornerRadius="4">
|
||||
<Grid Margin="0" ColumnDefinitions="*,Auto">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="5"
|
||||
Text="{Binding UserId}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Button Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Click="Recover"
|
||||
CommandParameter="{Binding}"
|
||||
Content="{ext:Locale Recover}"/>
|
||||
Margin="5"
|
||||
Command="{Binding Recover}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_RecoverProfile}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
@@ -56,12 +67,12 @@
|
||||
<TextBlock
|
||||
IsVisible="{Binding IsEmpty}"
|
||||
TextAlignment="Center"
|
||||
Text="{ext:Locale UserProfilesRecoverEmptyList}"/>
|
||||
Text="{ext:Locale UserProfiles_RecoverProfile_EmptyList}"/>
|
||||
</Panel>
|
||||
</Border>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="50"
|
||||
|
||||
@@ -27,11 +27,12 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
switch (arg.NavigationMode)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
case NavigationMode.Back:
|
||||
NavigationDialogHost parent = (NavigationDialogHost)arg.Parameter;
|
||||
|
||||
_parent = parent;
|
||||
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfilesRecoverHeading]}";
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfiles_RecoverLostProfiles]}";
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -42,10 +43,5 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
{
|
||||
_parent?.GoBack();
|
||||
}
|
||||
|
||||
private void Recover(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_parent?.RecoverLostAccounts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user