diff --git a/.github/labeler.yml b/.github/labeler.yml index 54f2757b0..871f9945f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -33,3 +33,11 @@ kernel: infra: - changed-files: - any-glob-to-any-file: ['.github/**', 'distribution/**', 'Directory.Packages.props'] + +documentation: +- changed-files: + - any-glob-to-any-file: 'docs/**' + +ldn: +- changed-files: + - any-glob-to-any-file: 'src/Ryujinx.HLE/HOS/Services/Ldn/**' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b11f0778..21dc3eb0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,36 +74,36 @@ jobs: chmod +x ./publish_sdl2_headless/Ryujinx.Headless.SDL2 ./publish_sdl2_headless/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 }}" + - 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 + sudo apt install -y zsync desktop-file-utils appstream - # mkdir -p tools - # export PATH="$PATH:$(readlink -f tools)" + 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 + # 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 + 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 + 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 @@ -112,17 +112,17 @@ jobs: 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 + - 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 - name: Upload Ryujinx.Headless.SDL2 artifact uses: actions/upload-artifact@v4 with: - name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }} + name: nogui-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }} path: publish_sdl2_headless if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13' @@ -185,6 +185,6 @@ jobs: - name: Upload Ryujinx.Headless.SDL2 artifact uses: actions/upload-artifact@v4 with: - name: sdl2-ryujinx-headless-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal + name: nogui-ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-macos_universal path: "publish_headless/*.tar.gz" if: github.event_name == 'pull_request' diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 000000000..df28e4784 --- /dev/null +++ b/.github/workflows/canary.yml @@ -0,0 +1,257 @@ +name: Canary release job + +on: + workflow_dispatch: + inputs: {} + push: + branches: [ master ] + paths-ignore: + - '.github/**' + - 'docs/**' + - 'assets/**' + - '*.yml' + - '*.json' + - '*.config' + - '*.md' + +concurrency: release + +env: + POWERSHELL_TELEMETRY_OPTOUT: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + RYUJINX_BASE_VERSION: "1.2" + RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "canary" + RYUJINX_TARGET_RELEASE_CHANNEL_OWNER: "GreemDev" + RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO: "Ryujinx" + RYUJINX_TARGET_RELEASE_CHANNEL_REPO: "Ryujinx-Canary" + RELEASE: 1 + +jobs: + tag: + name: Create tag + runs-on: ubuntu-20.04 + steps: + - name: Get version info + id: version_info + run: | + echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT + echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT + shell: bash + + - name: Create tag + uses: actions/github-script@v7 + with: + script: | + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/tags/Canary-${{ steps.version_info.outputs.build_version }}', + sha: context.sha + }) + + - name: Create release + uses: ncipollo/release-action@v1 + with: + name: "Canary ${{ steps.version_info.outputs.build_version }}" + tag: ${{ steps.version_info.outputs.build_version }} + body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }}" + omitBodyDuringUpdate: true + owner: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }} + repo: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }} + token: ${{ secrets.RELEASE_TOKEN }} + + release: + name: Release for ${{ matrix.platform.name }} + runs-on: ${{ matrix.platform.os }} + strategy: + matrix: + platform: + - { name: win-x64, os: windows-latest, zip_os_name: win_x64 } + - { 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@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 version info + id: version_info + run: | + echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT + echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT + echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT + shell: bash + + - name: Configure for release + run: | + sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs + shell: bash + + - name: Create output dir + run: "mkdir release_output" + + - name: Publish + run: | + dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained + dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained + + - name: Packing Windows builds + if: matrix.platform.os == 'windows-latest' + run: | + pushd publish_ava + rm publish/libarmeilleure-jitsupport.dylib + 7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish + popd + + pushd publish_sdl2_headless + rm publish/libarmeilleure-jitsupport.dylib + 7z a ../release_output/nogui-ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish + popd + shell: bash + + - name: Packing Linux builds + if: matrix.platform.os == 'ubuntu-latest' + run: | + pushd publish_ava + rm publish/libarmeilleure-jitsupport.dylib + chmod +x publish/Ryujinx.sh publish/Ryujinx + tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish + popd + + pushd publish_sdl2_headless + rm publish/libarmeilleure-jitsupport.dylib + chmod +x publish/Ryujinx.sh publish/Ryujinx.Headless.SDL2 + tar -czvf ../release_output/nogui-ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish + popd + shell: bash + + #- name: Build AppImage (Linux) + # if: matrix.platform.os == 'ubuntu-latest' + # run: | + # BUILD_VERSION="${{ steps.version_info.outputs.build_version }}" + # 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_ava OUTDIR=publish_ava_appimage distribution/linux/appimage/build-appimage.sh + + # Add to release output + # pushd publish_ava_appimage + # mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage + # mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync + # popd + # shell: bash + + - name: Pushing new release + uses: ncipollo/release-action@v1 + with: + name: ${{ steps.version_info.outputs.build_version }} + artifacts: "release_output/*.tar.gz,release_output/*.zip" + #artifacts: "release_output/*.tar.gz,release_output/*.zip/*AppImage*" + tag: ${{ steps.version_info.outputs.build_version }} + body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }}" + omitBodyDuringUpdate: true + allowUpdates: true + replacesArtifacts: true + owner: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }} + repo: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }} + token: ${{ secrets.RELEASE_TOKEN }} + + macos_release: + name: Release MacOS universal + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Setup LLVM 15 + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 15 + + - 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 version info + id: version_info + run: | + echo "build_version=${{ env.RYUJINX_BASE_VERSION }}.${{ github.run_number }}" >> $GITHUB_OUTPUT + echo "prev_build_version=${{ env.RYUJINX_BASE_VERSION }}.$((${{ github.run_number }} - 1))" >> $GITHUB_OUTPUT + echo "git_short_hash=$(git rev-parse --short "${{ github.sha }}")" >> $GITHUB_OUTPUT + + - name: Configure for release + run: | + sed -r --in-place 's/\%\%RYUJINX_BUILD_VERSION\%\%/${{ steps.version_info.outputs.build_version }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_BUILD_GIT_HASH\%\%/${{ steps.version_info.outputs.git_short_hash }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs + shell: bash + + - 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 + + - name: Publish macOS Ryujinx.Headless.SDL2 + run: | + ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 1 + + - name: Pushing new release + uses: ncipollo/release-action@v1 + with: + name: "Canary ${{ steps.version_info.outputs.build_version }}" + artifacts: "publish_ava/*.tar.gz, publish_headless/*.tar.gz" + tag: ${{ steps.version_info.outputs.build_version }} + body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/Canary-${{ steps.version_info.outputs.prev_build_version }}...Canary-${{ steps.version_info.outputs.build_version }}" + omitBodyDuringUpdate: true + allowUpdates: true + replacesArtifacts: true + owner: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }} + repo: ${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }} + token: ${{ secrets.RELEASE_TOKEN }} diff --git a/.github/workflows/nightly_pr_comment.yml b/.github/workflows/nightly_pr_comment.yml index 64705b6ee..85a6e2de4 100644 --- a/.github/workflows/nightly_pr_comment.yml +++ b/.github/workflows/nightly_pr_comment.yml @@ -38,12 +38,12 @@ jobs: return core.error(`No artifacts found`); } let body = `Download the artifacts for this pull request:\n`; - let hidden_headless_artifacts = `\n\n
GUI-less (SDL2)\n`; + let hidden_headless_artifacts = `\n\n
GUI-less\n`; let hidden_debug_artifacts = `\n\n
Only for Developers\n`; for (const art of artifacts) { if(art.name.includes('Debug')) { hidden_debug_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`; - } else if(art.name.includes('sdl2-ryujinx-headless')) { + } else if(art.name.includes('nogui-ryujinx')) { hidden_headless_artifacts += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`; } else { body += `\n* [${art.name}](https://nightly.link/${owner}/${repo}/actions/artifacts/${art.id}.zip)`; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aad32deb6..fbf715756 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: {} push: - branches: [ master ] + branches: [ release ] paths-ignore: - '.github/**' - 'docs/**' @@ -20,7 +20,7 @@ env: POWERSHELL_TELEMETRY_OPTOUT: 1 DOTNET_CLI_TELEMETRY_OPTOUT: 1 RYUJINX_BASE_VERSION: "1.2" - RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "master" + RYUJINX_TARGET_RELEASE_CHANNEL_NAME: "release" RYUJINX_TARGET_RELEASE_CHANNEL_OWNER: "GreemDev" RYUJINX_TARGET_RELEASE_CHANNEL_REPO: "Ryujinx" RELEASE: 1 @@ -93,6 +93,7 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash @@ -101,83 +102,79 @@ jobs: - name: Publish run: | - dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_ava/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained -p:IncludeNativeLibrariesForSelfExtract=true - dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless/publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained -p:IncludeNativeLibrariesForSelfExtract=true + dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx --self-contained + dotnet publish -c Release -r "${{ matrix.platform.name }}" -o ./publish_sdl2_headless -p:Version="${{ steps.version_info.outputs.build_version }}" -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:DebugType=embedded src/Ryujinx.Headless.SDL2 --self-contained - name: Packing Windows builds if: matrix.platform.os == 'windows-latest' run: | - pushd publish_ava - rm publish/libarmeilleure-jitsupport.dylib - 7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish + pushd publish + rm libarmeilleure-jitsupport.dylib + 7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish popd pushd publish_sdl2_headless - rm publish/libarmeilleure-jitsupport.dylib - 7z a ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish + rm libarmeilleure-jitsupport.dylib + 7z a ../release_output/nogui-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish popd shell: bash + + - name: Build AppImage (Linux) + if: matrix.platform.os == 'ubuntu-latest' + run: | + BUILD_VERSION="${{ steps.version_info.outputs.build_version }}" + 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 + + pushd publish_appimage + mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage + mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync + popd + shell: bash - name: Packing Linux builds if: matrix.platform.os == 'ubuntu-latest' run: | - pushd publish_ava - rm publish/libarmeilleure-jitsupport.dylib - chmod +x publish/Ryujinx.sh publish/Ryujinx - tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish + pushd publish + chmod +x Ryujinx.sh Ryujinx + tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish popd pushd publish_sdl2_headless - rm publish/libarmeilleure-jitsupport.dylib - chmod +x publish/Ryujinx.sh publish/Ryujinx.Headless.SDL2 - tar -czvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish + chmod +x Ryujinx.sh Ryujinx.Headless.SDL2 + tar -czvf ../release_output/nogui-ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish popd shell: bash - - #- name: Build AppImage (Linux) - # if: matrix.platform.os == 'ubuntu-latest' - # run: | - # BUILD_VERSION="${{ steps.version_info.outputs.build_version }}" - # 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_ava OUTDIR=publish_ava_appimage distribution/linux/appimage/build-appimage.sh - - # Add to release output - # pushd publish_ava_appimage - # mv Ryujinx.AppImage ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage - # mv Ryujinx.AppImage.zsync ../release_output/ryujinx-$BUILD_VERSION-$ARCH_NAME.AppImage.zsync - # popd - # shell: bash - name: Pushing new release uses: ncipollo/release-action@v1 with: name: ${{ steps.version_info.outputs.build_version }} - artifacts: "release_output/*.tar.gz,release_output/*.zip" - #artifacts: "release_output/*.tar.gz,release_output/*.zip/*AppImage*" + artifacts: "release_output/*.tar.gz,release_output/*.zip,release_output/*AppImage*" tag: ${{ steps.version_info.outputs.build_version }} body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}" omitBodyDuringUpdate: true @@ -228,22 +225,23 @@ jobs: sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs + sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs shell: bash - 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 + ./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: Publish macOS Ryujinx.Headless.SDL2 run: | - ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release + ./distribution/macos/create_macos_build_headless.sh . publish_tmp_headless publish_headless ./distribution/macos/entitlements.xml "${{ steps.version_info.outputs.build_version }}" "${{ steps.version_info.outputs.git_short_hash }}" Release 0 - name: Pushing new release uses: ncipollo/release-action@v1 with: name: ${{ steps.version_info.outputs.build_version }} - artifacts: "publish_ava/*.tar.gz, publish_headless/*.tar.gz" + artifacts: "publish/*.tar.gz, publish_headless/*.tar.gz" tag: ${{ steps.version_info.outputs.build_version }} body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}" omitBodyDuringUpdate: true diff --git a/.gitignore b/.gitignore index f71237b1a..9a192926f 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,6 @@ PublishProfiles/ # Glade backup files *.glade~ + +# Ignore MacOS Attribute Files +._* diff --git a/COMPILING.md b/COMPILING.md new file mode 100644 index 000000000..20a2eb7ff --- /dev/null +++ b/COMPILING.md @@ -0,0 +1,23 @@ +## Compilation + +Building the project is for users that want to contribute code only. +If you wish to build the emulator yourself, follow these steps: + +### Step 1 + +Install the [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0). +Make sure your SDK version is higher or equal to the required version specified in [global.json](global.json). + +### Step 2 + +Either use `git clone https://github.com/GreemDev/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files. + +### Step 3 + +To build Ryujinx, open a command prompt inside the project directory. +You can quickly access it on Windows by holding shift in File Explorer, then right clicking and selecting `Open command window here`. +Then type the following command: `dotnet build -c Release -o build` +the built files will be found in the newly created build directory. + +Ryujinx system files are stored in the `Ryujinx` folder. +This folder is located in the user folder, which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af47fc9d9..686ea3994 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,7 @@ We use and recommend the following workflow: 3. In your fork, create a branch off of main (`git checkout -b mybranch`). - Branches are useful since they isolate your changes from incoming changes from upstream. They also enable you to create multiple PRs from the same fork. 4. Make and commit your changes to your branch. - - [Build Instructions](https://github.com/GreemDev/Ryujinx#building) explains how to build and test. + - [Build Instructions](https://github.com/GreemDev/Ryujinx/blob/master/COMPILING.md) explains how to build and test. - Commit messages should be clear statements of action and intent. 6. Build the repository with your changes. - Make sure that the builds are clean. @@ -83,7 +83,7 @@ We use and recommend the following workflow: - State in the description what issue or improvement your change is addressing. - Check if all the Continuous Integration checks are passing. Refer to [Actions](https://github.com/GreemDev/Ryujinx/actions) to check for outstanding errors. 8. Wait for feedback or approval of your changes from the core development team - - Details about the pull request [review procedure](docs/workflow/ci/pr-guide.md). + - Details about the pull request [review procedure](docs/workflow/pr-guide.md). 9. When the team members have signed off, and all checks are green, your PR will be merged. - The next official build will automatically include your change. - You can delete the branch you used for making the change. diff --git a/Directory.Packages.props b/Directory.Packages.props index e6be60790..ffb5f2ead 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + @@ -33,11 +33,12 @@ + - + @@ -51,4 +52,4 @@ - \ No newline at end of file + diff --git a/README.md b/README.md index f03a1205f..f6783b412 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ Latest Release +
+ + + + + Latest Canary Release +

@@ -33,7 +42,7 @@ Guides and documentation can be found on the Wiki tab.

- If you would like a version more preservative fork of Ryujinx, check out ryujinx-mirror. + If you would like a more preservative fork of Ryujinx, check out ryujinx-mirror.

@@ -47,15 +56,6 @@

-## Compatibility - -As of May 2024, Ryujinx has been tested on approximately 4,300 titles; -over 4,100 boot past menus and into gameplay, with roughly 3,550 of those being considered playable. - -Anyone is free to submit a new game test or update an existing game test entry; -simply follow the new issue template and testing guidelines, or post as a reply to the applicable game issue. -Use the search function to see if a game has been tested already! - ## Usage To run this emulator, your PC must be equipped with at least 8GiB of RAM; @@ -63,38 +63,21 @@ failing to meet this requirement may result in a poor gameplay experience or une ## Latest build -These 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, our automated builds **may be unstable or completely broken**. +Stable builds are made every so often onto a separate "release" branch that then gets put into the releases you know and love. +These stable builds exist so that the end user can get a more **enjoyable and stable experience**. -You can find the latest release [here](https://github.com/GreemDev/Ryujinx/releases/latest). +You can find the latest stable release [here](https://github.com/GreemDev/Ryujinx/releases/latest). + +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 latest canary release [here](https://github.com/GreemDev/Ryujinx-Canary/releases/latest). ## Documentation If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md). -## Building - -If you wish to build the emulator yourself, follow these steps: - -### Step 1 - -Install the [.NET 8.0 (or higher) SDK](https://dotnet.microsoft.com/download/dotnet/8.0). -Make sure your SDK version is higher or equal to the required version specified in [global.json](global.json). - -### Step 2 - -Either use `git clone https://github.com/GreemDev/Ryujinx` on the command line to clone the repository or use Code --> Download zip button to get the files. - -### Step 3 - -To build Ryujinx, open a command prompt inside the project directory. -You can quickly access it on Windows by holding shift in File Explorer, then right clicking and selecting `Open command window here`. -Then type the following command: `dotnet build -c Release -o build` -the built files will be found in the newly created build directory. - -Ryujinx system files are stored in the `Ryujinx` folder. -This folder is located in the user folder, which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI. - ## Features - **Audio** diff --git a/Ryujinx.sln b/Ryujinx.sln index eaa28f8dd..d661b903c 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -29,14 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio", "src\Ryujinx.Audio\Ryujinx.Audio.csproj", "{806ACF6D-90B0-45D0-A1AC-5F220F3B3985}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - Directory.Packages.props = Directory.Packages.props - Release Script = .github/workflows/release.yml - Build Script = .github/workflows/build.yml - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Memory", "src\Ryujinx.Memory\Ryujinx.Memory.csproj", "{A5E6C691-9E22-4263-8F40-42F002CE66BE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests.Memory", "src\Ryujinx.Tests.Memory\Ryujinx.Tests.Memory.csproj", "{D1CC5322-7325-4F6B-9625-194B30BE1296}" @@ -89,6 +81,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Gene EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Packages.props = Directory.Packages.props + .github/workflows/release.yml = .github/workflows/release.yml + .github/workflows/canary.yml = .github/workflows/canary.yml + .github/workflows/build.yml = .github/workflows/build.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/assets/amiibo/Amiibo.json b/assets/amiibo/Amiibo.json index b877ea142..03c2c020e 100644 --- a/assets/amiibo/Amiibo.json +++ b/assets/amiibo/Amiibo.json @@ -707,6 +707,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01010300", @@ -3526,6 +3542,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01400000", @@ -4160,6 +4192,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -5848,6 +5896,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -6126,6 +6190,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -8341,6 +8421,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -9020,6 +9116,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000100", @@ -9496,6 +9608,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01010000", @@ -9833,6 +9961,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -14667,6 +14811,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01030000", @@ -16119,6 +16279,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01050000", @@ -16717,6 +16893,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01070000", @@ -19745,6 +19937,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01080000", @@ -20503,6 +20711,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01010000", @@ -21805,6 +22029,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -22340,6 +22580,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01020100", @@ -22990,6 +23246,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -23440,6 +23712,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -24660,6 +24948,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01410000", @@ -24954,6 +25258,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01060000", @@ -25286,6 +25606,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -29114,6 +29450,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01010000", @@ -32512,6 +32864,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -32928,6 +33296,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000100", @@ -34800,6 +35184,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Red Tunic", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01000000", @@ -37569,6 +37969,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01010100", @@ -41293,6 +41709,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Black Cat Clothes", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01020100", @@ -45153,6 +45585,22 @@ "0100F2C0115B6000" ], "gameName": "The Legend of Zelda: Tears of the Kingdom" + }, + { + "amiiboUsage": [ + { + "Usage": "Receive the Blue Attire", + "write": false + }, + { + "Usage": "Receive random materials", + "write": false + } + ], + "gameID": [ + "01008CF01BAAC000" + ], + "gameName": "The Legend of Zelda: Echoes of Wisdom" } ], "head": "01010000", @@ -47896,5 +48344,5 @@ "type": "Figure" } ], - "lastUpdated": "2024-10-01T00:00:25.035619" -} \ No newline at end of file + "lastUpdated": "2024-11-17T15:28:47.035619" +} diff --git a/distribution/linux/Ryujinx.sh b/distribution/linux/Ryujinx.sh index 30eb14399..daeea9bfd 100755 --- a/distribution/linux/Ryujinx.sh +++ b/distribution/linux/Ryujinx.sh @@ -14,7 +14,7 @@ if [ -z "$RYUJINX_BIN" ]; then exit 1 fi -COMMAND="env DOTNET_EnableAlternateStackCheck=1" +COMMAND="env LANG=C.UTF-8 DOTNET_EnableAlternateStackCheck=1" if command -v gamemoderun > /dev/null 2>&1; then COMMAND="$COMMAND gamemoderun" diff --git a/distribution/macos/create_app_bundle.sh b/distribution/macos/create_app_bundle.sh index 0fa54eadd..e4397da84 100755 --- a/distribution/macos/create_app_bundle.sh +++ b/distribution/macos/create_app_bundle.sh @@ -46,5 +46,5 @@ then rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$APP_BUNDLE_DIRECTORY" else echo "Usign codesign for ad-hoc signing" - codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$APP_BUNDLE_DIRECTORY" -fi \ No newline at end of file + codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$APP_BUNDLE_DIRECTORY" +fi diff --git a/distribution/macos/create_macos_build_ava.sh b/distribution/macos/create_macos_build_ava.sh index 6785cbb23..b19fa4863 100755 --- a/distribution/macos/create_macos_build_ava.sh +++ b/distribution/macos/create_macos_build_ava.sh @@ -2,8 +2,8 @@ set -e -if [ "$#" -lt 7 ]; then - echo "usage " +if [ "$#" -lt 8 ]; then + echo "usage " exit 1 fi @@ -18,10 +18,11 @@ ENTITLEMENTS_FILE_PATH=$(readlink -f "$4") VERSION=$5 SOURCE_REVISION_ID=$6 CONFIGURATION=$7 -EXTRA_ARGS=$8 +CANARY=$8 -if [ "$VERSION" == "1.1.0" ]; -then +if [ "$CANARY" == "1" ]; then + RELEASE_TAR_FILE_NAME=ryujinx-canary-$VERSION-macos_universal.app.tar +elif [ "$VERSION" == "1.1.0" ]; then RELEASE_TAR_FILE_NAME=ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.app.tar else RELEASE_TAR_FILE_NAME=ryujinx-$VERSION-macos_universal.app.tar @@ -61,7 +62,7 @@ mkdir -p "$OUTPUT_DIRECTORY" cp -R "$ARM64_APP_BUNDLE" "$UNIVERSAL_APP_BUNDLE" rm "$UNIVERSAL_APP_BUNDLE/$EXECUTABLE_SUB_PATH" -# Make it libraries universal +# 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)" ]; @@ -99,7 +100,7 @@ then 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 --deep -s - "$UNIVERSAL_APP_BUNDLE" + codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$UNIVERSAL_APP_BUNDLE" fi echo "Creating archive" @@ -111,4 +112,4 @@ rm "$RELEASE_TAR_FILE_NAME" popd -echo "Done" \ No newline at end of file +echo "Done" diff --git a/distribution/macos/create_macos_build_headless.sh b/distribution/macos/create_macos_build_headless.sh index a439aef45..01951d878 100755 --- a/distribution/macos/create_macos_build_headless.sh +++ b/distribution/macos/create_macos_build_headless.sh @@ -2,8 +2,8 @@ set -e -if [ "$#" -lt 7 ]; then - echo "usage " +if [ "$#" -lt 8 ]; then + echo "usage " exit 1 fi @@ -18,13 +18,14 @@ ENTITLEMENTS_FILE_PATH=$(readlink -f "$4") VERSION=$5 SOURCE_REVISION_ID=$6 CONFIGURATION=$7 -EXTRA_ARGS=$8 +CANARY=$8 -if [ "$VERSION" == "1.1.0" ]; -then - RELEASE_TAR_FILE_NAME=sdl2-ryujinx-headless-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.tar +if [ "$CANARY" == "1" ]; then + RELEASE_TAR_FILE_NAME=nogui-ryujinx-canary-$VERSION-macos_universal.tar +elif [ "$VERSION" == "1.1.0" ]; then + RELEASE_TAR_FILE_NAME=nogui-ryujinx-$CONFIGURATION-$VERSION+$SOURCE_REVISION_ID-macos_universal.tar else - RELEASE_TAR_FILE_NAME=sdl2-ryujinx-headless-$VERSION-macos_universal.tar + RELEASE_TAR_FILE_NAME=nogui-ryujinx-$VERSION-macos_universal.tar fi ARM64_OUTPUT="$TEMP_DIRECTORY/publish_arm64" @@ -56,7 +57,7 @@ mkdir -p "$OUTPUT_DIRECTORY" cp -R "$ARM64_OUTPUT/" "$UNIVERSAL_OUTPUT" rm "$UNIVERSAL_OUTPUT/$EXECUTABLE_SUB_PATH" -# Make it libraries universal +# Make its libraries universal python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_OUTPUT" "$X64_OUTPUT" "$UNIVERSAL_OUTPUT" "**/*.dylib" if ! [ -x "$(command -v lipo)" ]; @@ -95,7 +96,7 @@ else echo "Using codesign for ad-hoc signing" for FILE in "$UNIVERSAL_OUTPUT"/*; do if [[ $(file "$FILE") == *"Mach-O"* ]]; then - codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$FILE" + codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$FILE" fi done fi @@ -108,4 +109,4 @@ gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz" rm "$RELEASE_TAR_FILE_NAME" popd -echo "Done" \ No newline at end of file +echo "Done" diff --git a/docs/workflow/pr-guide.md b/docs/workflow/pr-guide.md index c03db210b..50f44d87f 100644 --- a/docs/workflow/pr-guide.md +++ b/docs/workflow/pr-guide.md @@ -9,7 +9,7 @@ To merge pull requests, you must have write permissions in the repository. ## Quick Code Review Rules * Do not mix unrelated changes in one pull request. For example, a code style change should never be mixed with a bug fix. -* All changes should follow the existing code style. You can read more about our code style at [docs/coding-guidelines](../coding-guidelines/coding-style.md). +* All changes should follow the existing code style. You can read more about our code style at [docs/coding-style](../coding-guidelines/coding-style.md). * Adding external dependencies is to be avoided unless not doing so would introduce _significant_ complexity. Any dependency addition should be justified and discussed before merge. * Use Draft pull requests for changes you are still working on but want early CI loop feedback. When you think your changes are ready for review, [change the status](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request) of your pull request. * Rebase your changes when required or directly requested. Changes should always be commited on top of the upstream branch, not the other way around. diff --git a/src/ARMeilleure/ARMeilleure.csproj b/src/ARMeilleure/ARMeilleure.csproj index 550e50c26..4b67fdb3b 100644 --- a/src/ARMeilleure/ARMeilleure.csproj +++ b/src/ARMeilleure/ARMeilleure.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/ARMeilleure/Common/AddressTable.cs b/src/ARMeilleure/Common/AddressTable.cs deleted file mode 100644 index a3ffaf470..000000000 --- a/src/ARMeilleure/Common/AddressTable.cs +++ /dev/null @@ -1,252 +0,0 @@ -using ARMeilleure.Diagnostics; -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace ARMeilleure.Common -{ - /// - /// Represents a table of guest address to a value. - /// - /// Type of the value - public unsafe class AddressTable : IDisposable where TEntry : unmanaged - { - /// - /// Represents a level in an . - /// - public readonly struct Level - { - /// - /// Gets the index of the in the guest address. - /// - public int Index { get; } - - /// - /// Gets the length of the in the guest address. - /// - public int Length { get; } - - /// - /// Gets the mask which masks the bits used by the . - /// - public ulong Mask => ((1ul << Length) - 1) << Index; - - /// - /// Initializes a new instance of the structure with the specified - /// and . - /// - /// Index of the - /// Length of the - public Level(int index, int length) - { - (Index, Length) = (index, length); - } - - /// - /// Gets the value of the from the specified guest . - /// - /// Guest address - /// Value of the from the specified guest - public int GetValue(ulong address) - { - return (int)((address & Mask) >> Index); - } - } - - private bool _disposed; - private TEntry** _table; - private readonly List _pages; - - /// - /// Gets the bits used by the of the instance. - /// - public ulong Mask { get; } - - /// - /// Gets the s used by the instance. - /// - public Level[] Levels { get; } - - /// - /// Gets or sets the default fill value of newly created leaf pages. - /// - public TEntry Fill { get; set; } - - /// - /// Gets the base address of the . - /// - /// instance was disposed - public nint Base - { - get - { - ObjectDisposedException.ThrowIf(_disposed, this); - - lock (_pages) - { - return (nint)GetRootPage(); - } - } - } - - /// - /// Constructs a new instance of the class with the specified list of - /// . - /// - /// is null - /// Length of is less than 2 - public AddressTable(Level[] levels) - { - ArgumentNullException.ThrowIfNull(levels); - - if (levels.Length < 2) - { - throw new ArgumentException("Table must be at least 2 levels deep.", nameof(levels)); - } - - _pages = new List(capacity: 16); - - Levels = levels; - Mask = 0; - - foreach (var level in Levels) - { - Mask |= level.Mask; - } - } - - /// - /// Determines if the specified is in the range of the - /// . - /// - /// Guest address - /// if is valid; otherwise - public bool IsValid(ulong address) - { - return (address & ~Mask) == 0; - } - - /// - /// Gets a reference to the value at the specified guest . - /// - /// Guest address - /// Reference to the value at the specified guest - /// instance was disposed - /// is not mapped - public ref TEntry GetValue(ulong address) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - if (!IsValid(address)) - { - throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address)); - } - - lock (_pages) - { - return ref GetPage(address)[Levels[^1].GetValue(address)]; - } - } - - /// - /// Gets the leaf page for the specified guest . - /// - /// Guest address - /// Leaf page for the specified guest - private TEntry* GetPage(ulong address) - { - TEntry** page = GetRootPage(); - - for (int i = 0; i < Levels.Length - 1; i++) - { - ref Level level = ref Levels[i]; - ref TEntry* nextPage = ref page[level.GetValue(address)]; - - if (nextPage == null) - { - ref Level nextLevel = ref Levels[i + 1]; - - nextPage = i == Levels.Length - 2 ? - (TEntry*)Allocate(1 << nextLevel.Length, Fill, leaf: true) : - (TEntry*)Allocate(1 << nextLevel.Length, nint.Zero, leaf: false); - } - - page = (TEntry**)nextPage; - } - - return (TEntry*)page; - } - - /// - /// Lazily initialize and get the root page of the . - /// - /// Root page of the - private TEntry** GetRootPage() - { - if (_table == null) - { - _table = (TEntry**)Allocate(1 << Levels[0].Length, fill: nint.Zero, leaf: false); - } - - return _table; - } - - /// - /// Allocates a block of memory of the specified type and length. - /// - /// Type of elements - /// Number of elements - /// Fill value - /// if leaf; otherwise - /// Allocated block - private nint Allocate(int length, T fill, bool leaf) where T : unmanaged - { - var size = sizeof(T) * length; - var page = (nint)NativeAllocator.Instance.Allocate((uint)size); - var span = new Span((void*)page, length); - - span.Fill(fill); - - _pages.Add(page); - - TranslatorEventSource.Log.AddressTableAllocated(size, leaf); - - return page; - } - - /// - /// Releases all resources used by the instance. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases all unmanaged and optionally managed resources used by the - /// instance. - /// - /// to dispose managed resources also; otherwise just unmanaged resouces - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - foreach (var page in _pages) - { - Marshal.FreeHGlobal(page); - } - - _disposed = true; - } - } - - /// - /// Frees resources used by the instance. - /// - ~AddressTable() - { - Dispose(false); - } - } -} diff --git a/src/ARMeilleure/Common/AddressTableLevel.cs b/src/ARMeilleure/Common/AddressTableLevel.cs new file mode 100644 index 000000000..af3b9b99f --- /dev/null +++ b/src/ARMeilleure/Common/AddressTableLevel.cs @@ -0,0 +1,44 @@ +namespace ARMeilleure.Common +{ + /// + /// Represents a level in an . + /// + public readonly struct AddressTableLevel + { + /// + /// Gets the index of the in the guest address. + /// + public int Index { get; } + + /// + /// Gets the length of the in the guest address. + /// + public int Length { get; } + + /// + /// Gets the mask which masks the bits used by the . + /// + public ulong Mask => ((1ul << Length) - 1) << Index; + + /// + /// Initializes a new instance of the structure with the specified + /// and . + /// + /// Index of the + /// Length of the + public AddressTableLevel(int index, int length) + { + (Index, Length) = (index, length); + } + + /// + /// Gets the value of the from the specified guest . + /// + /// Guest address + /// Value of the from the specified guest + public long GetValue(ulong address) + { + return (long)((address & Mask) >> Index); + } + } +} diff --git a/src/ARMeilleure/Common/AddressTablePresets.cs b/src/ARMeilleure/Common/AddressTablePresets.cs new file mode 100644 index 000000000..977e84a36 --- /dev/null +++ b/src/ARMeilleure/Common/AddressTablePresets.cs @@ -0,0 +1,75 @@ +namespace ARMeilleure.Common +{ + public static class AddressTablePresets + { + private static readonly AddressTableLevel[] _levels64Bit = + new AddressTableLevel[] + { + new(31, 17), + new(23, 8), + new(15, 8), + new( 7, 8), + new( 2, 5), + }; + + private static readonly AddressTableLevel[] _levels32Bit = + new AddressTableLevel[] + { + new(31, 17), + new(23, 8), + new(15, 8), + new( 7, 8), + new( 1, 6), + }; + + private static readonly AddressTableLevel[] _levels64BitSparseTiny = + new AddressTableLevel[] + { + new( 11, 28), + new( 2, 9), + }; + + private static readonly AddressTableLevel[] _levels32BitSparseTiny = + new AddressTableLevel[] + { + new( 10, 22), + new( 1, 9), + }; + + private static readonly AddressTableLevel[] _levels64BitSparseGiant = + new AddressTableLevel[] + { + new( 38, 1), + new( 2, 36), + }; + + private static readonly AddressTableLevel[] _levels32BitSparseGiant = + new AddressTableLevel[] + { + new( 31, 1), + new( 1, 30), + }; + + //high power will run worse on DDR3 systems and some DDR4 systems due to the higher ram utilization + //low power will never run worse than non-sparse, but for most systems it won't be necessary + //high power is always used, but I've left low power in here for future reference + public static AddressTableLevel[] GetArmPreset(bool for64Bits, bool sparse, bool lowPower = false) + { + if (sparse) + { + if (lowPower) + { + return for64Bits ? _levels64BitSparseTiny : _levels32BitSparseTiny; + } + else + { + return for64Bits ? _levels64BitSparseGiant : _levels32BitSparseGiant; + } + } + else + { + return for64Bits ? _levels64Bit : _levels32Bit; + } + } + } +} diff --git a/src/ARMeilleure/Common/Allocator.cs b/src/ARMeilleure/Common/Allocator.cs index 6905a614f..de6a77ebe 100644 --- a/src/ARMeilleure/Common/Allocator.cs +++ b/src/ARMeilleure/Common/Allocator.cs @@ -2,7 +2,7 @@ using System; namespace ARMeilleure.Common { - unsafe abstract class Allocator : IDisposable + public unsafe abstract class Allocator : IDisposable { public T* Allocate(ulong count = 1) where T : unmanaged { diff --git a/src/ARMeilleure/Common/IAddressTable.cs b/src/ARMeilleure/Common/IAddressTable.cs new file mode 100644 index 000000000..65077ec43 --- /dev/null +++ b/src/ARMeilleure/Common/IAddressTable.cs @@ -0,0 +1,51 @@ +using System; + +namespace ARMeilleure.Common +{ + public interface IAddressTable : IDisposable where TEntry : unmanaged + { + /// + /// True if the address table's bottom level is sparsely mapped. + /// This also ensures the second bottom level is filled with a dummy page rather than 0. + /// + bool Sparse { get; } + + /// + /// Gets the bits used by the of the instance. + /// + ulong Mask { get; } + + /// + /// Gets the s used by the instance. + /// + AddressTableLevel[] Levels { get; } + + /// + /// Gets or sets the default fill value of newly created leaf pages. + /// + TEntry Fill { get; set; } + + /// + /// Gets the base address of the . + /// + /// instance was disposed + nint Base { get; } + + /// + /// Determines if the specified is in the range of the + /// . + /// + /// Guest address + /// if is valid; otherwise + bool IsValid(ulong address); + + /// + /// Gets a reference to the value at the specified guest . + /// + /// Guest address + /// Reference to the value at the specified guest + /// instance was disposed + /// is not mapped + ref TEntry GetValue(ulong address); + } +} diff --git a/src/ARMeilleure/Common/NativeAllocator.cs b/src/ARMeilleure/Common/NativeAllocator.cs index ca5d3a850..ffcffa4bc 100644 --- a/src/ARMeilleure/Common/NativeAllocator.cs +++ b/src/ARMeilleure/Common/NativeAllocator.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace ARMeilleure.Common { - unsafe sealed class NativeAllocator : Allocator + public unsafe sealed class NativeAllocator : Allocator { public static NativeAllocator Instance { get; } = new(); diff --git a/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs b/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs index 2009bafda..a602ea49e 100644 --- a/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs +++ b/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs @@ -193,6 +193,8 @@ namespace ARMeilleure.Instructions Operand hostAddress; + var table = context.FunctionTable; + // If address is mapped onto the function table, we can skip the table walk. Otherwise we fallback // onto the dispatch stub. if (guestAddress.Kind == OperandKind.Constant && context.FunctionTable.IsValid(guestAddress.Value)) @@ -203,6 +205,30 @@ namespace ARMeilleure.Instructions hostAddress = context.Load(OperandType.I64, hostAddressAddr); } + else if (table.Sparse) + { + // Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels. + // Deliberately attempts to avoid branches. + + Operand tableBase = !context.HasPtc ? + Const(table.Base) : + Const(table.Base, Ptc.FunctionTableSymbol); + + hostAddress = tableBase; + + for (int i = 0; i < table.Levels.Length; i++) + { + var level = table.Levels[i]; + int clearBits = 64 - (level.Index + level.Length); + + Operand index = context.ShiftLeft( + context.ShiftRightUI(context.ShiftLeft(guestAddress, Const(clearBits)), Const(clearBits + level.Index)), + Const(3) + ); + + hostAddress = context.Load(OperandType.I64, context.Add(hostAddress, index)); + } + } else { hostAddress = !context.HasPtc ? diff --git a/src/ARMeilleure/Instructions/InstEmitSystem.cs b/src/ARMeilleure/Instructions/InstEmitSystem.cs index 8c430fc23..11c1d0328 100644 --- a/src/ARMeilleure/Instructions/InstEmitSystem.cs +++ b/src/ARMeilleure/Instructions/InstEmitSystem.cs @@ -49,6 +49,9 @@ namespace ARMeilleure.Instructions case 0b11_011_1101_0000_011: EmitGetTpidrroEl0(context); return; + case 0b11_011_1101_0000_101: + EmitGetTpidr2El0(context); + return; case 0b11_011_1110_0000_000: info = typeof(NativeInterface).GetMethod(nameof(NativeInterface.GetCntfrqEl0)); break; @@ -84,6 +87,9 @@ namespace ARMeilleure.Instructions case 0b11_011_1101_0000_010: EmitSetTpidrEl0(context); return; + case 0b11_011_1101_0000_101: + EmitSetTpidr2El0(context); + return; default: throw new NotImplementedException($"Unknown MSR 0x{op.RawOpCode:X8} at 0x{op.Address:X16}."); @@ -213,6 +219,17 @@ namespace ARMeilleure.Instructions SetIntOrZR(context, op.Rt, result); } + private static void EmitGetTpidr2El0(ArmEmitterContext context) + { + OpCodeSystem op = (OpCodeSystem)context.CurrOp; + + Operand nativeContext = context.LoadArgument(OperandType.I64, 0); + + Operand result = context.Load(OperandType.I64, context.Add(nativeContext, Const((ulong)NativeContext.GetTpidr2El0Offset()))); + + SetIntOrZR(context, op.Rt, result); + } + private static void EmitSetNzcv(ArmEmitterContext context) { OpCodeSystem op = (OpCodeSystem)context.CurrOp; @@ -274,5 +291,16 @@ namespace ARMeilleure.Instructions context.Store(context.Add(nativeContext, Const((ulong)NativeContext.GetTpidrEl0Offset())), value); } + + private static void EmitSetTpidr2El0(ArmEmitterContext context) + { + OpCodeSystem op = (OpCodeSystem)context.CurrOp; + + Operand value = GetIntOrZR(context, op.Rt); + + Operand nativeContext = context.LoadArgument(OperandType.I64, 0); + + context.Store(context.Add(nativeContext, Const((ulong)NativeContext.GetTpidr2El0Offset())), value); + } } } diff --git a/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs index 1b3689e3f..35747d7a4 100644 --- a/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs +++ b/src/ARMeilleure/Signal/NativeSignalHandlerGenerator.cs @@ -8,7 +8,7 @@ namespace ARMeilleure.Signal { public static class NativeSignalHandlerGenerator { - public const int MaxTrackedRanges = 8; + public const int MaxTrackedRanges = 16; private const int StructAddressOffset = 0; private const int StructWriteOffset = 4; diff --git a/src/ARMeilleure/State/NativeContext.cs b/src/ARMeilleure/State/NativeContext.cs index 628efde41..140b6f7a7 100644 --- a/src/ARMeilleure/State/NativeContext.cs +++ b/src/ARMeilleure/State/NativeContext.cs @@ -21,6 +21,7 @@ namespace ARMeilleure.State public ulong ExclusiveValueLow; public ulong ExclusiveValueHigh; public int Running; + public long Tpidr2El0; } private static NativeCtxStorage _dummyStorage = new(); @@ -176,6 +177,9 @@ namespace ARMeilleure.State public long GetTpidrroEl0() => GetStorage().TpidrroEl0; public void SetTpidrroEl0(long value) => GetStorage().TpidrroEl0 = value; + public long GetTpidr2El0() => GetStorage().Tpidr2El0; + public void SetTpidr2El0(long value) => GetStorage().Tpidr2El0 = value; + public int GetCounter() => GetStorage().Counter; public void SetCounter(int value) => GetStorage().Counter = value; @@ -232,6 +236,11 @@ namespace ARMeilleure.State return StorageOffset(ref _dummyStorage, ref _dummyStorage.TpidrroEl0); } + public static int GetTpidr2El0Offset() + { + return StorageOffset(ref _dummyStorage, ref _dummyStorage.Tpidr2El0); + } + public static int GetCounterOffset() { return StorageOffset(ref _dummyStorage, ref _dummyStorage.Counter); diff --git a/src/ARMeilleure/Translation/ArmEmitterContext.cs b/src/ARMeilleure/Translation/ArmEmitterContext.cs index 5d79171a2..82f12bb02 100644 --- a/src/ARMeilleure/Translation/ArmEmitterContext.cs +++ b/src/ARMeilleure/Translation/ArmEmitterContext.cs @@ -46,7 +46,7 @@ namespace ARMeilleure.Translation public IMemoryManager Memory { get; } public EntryTable CountTable { get; } - public AddressTable FunctionTable { get; } + public IAddressTable FunctionTable { get; } public TranslatorStubs Stubs { get; } public ulong EntryAddress { get; } @@ -62,7 +62,7 @@ namespace ARMeilleure.Translation public ArmEmitterContext( IMemoryManager memory, EntryTable countTable, - AddressTable funcTable, + IAddressTable funcTable, TranslatorStubs stubs, ulong entryAddress, bool highCq, diff --git a/src/ARMeilleure/Translation/Dominance.cs b/src/ARMeilleure/Translation/Dominance.cs index e2185bd85..b62714fdf 100644 --- a/src/ARMeilleure/Translation/Dominance.cs +++ b/src/ARMeilleure/Translation/Dominance.cs @@ -77,7 +77,7 @@ namespace ARMeilleure.Translation { continue; } - + for (int pBlkIndex = 0; pBlkIndex < block.Predecessors.Count; pBlkIndex++) { BasicBlock current = block.Predecessors[pBlkIndex]; diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index 6dd38ed89..841e5fefa 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.Linq; using System.Runtime; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -29,7 +30,7 @@ namespace ARMeilleure.Translation.PTC private const string OuterHeaderMagicString = "PTCohd\0\0"; private const string InnerHeaderMagicString = "PTCihd\0\0"; - private const uint InternalVersion = 6950; //! To be incremented manually for each change to the ARMeilleure project. + private const uint InternalVersion = 6997; //! To be incremented manually for each change to the ARMeilleure project. private const string ActualDir = "0"; private const string BackupDir = "1"; @@ -40,6 +41,7 @@ namespace ARMeilleure.Translation.PTC public static readonly Symbol PageTableSymbol = new(SymbolType.Special, 1); public static readonly Symbol CountTableSymbol = new(SymbolType.Special, 2); public static readonly Symbol DispatchStubSymbol = new(SymbolType.Special, 3); + public static readonly Symbol FunctionTableSymbol = new(SymbolType.Special, 4); private const byte FillingByte = 0x00; private const CompressionLevel SaveCompressionLevel = CompressionLevel.Fastest; @@ -100,7 +102,7 @@ namespace ARMeilleure.Translation.PTC Disable(); } - public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode) + public void Initialize(string titleIdText, string displayVersion, bool enabled, MemoryManagerType memoryMode, string cacheSelector) { Wait(); @@ -126,6 +128,8 @@ namespace ARMeilleure.Translation.PTC DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault; _memoryMode = memoryMode; + Logger.Info?.Print(LogClass.Ptc, $"PPTC (v{InternalVersion}) Profile: {DisplayVersion}-{cacheSelector}"); + string workPathActual = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", ActualDir); string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", BackupDir); @@ -139,8 +143,8 @@ namespace ARMeilleure.Translation.PTC Directory.CreateDirectory(workPathBackup); } - CachePathActual = Path.Combine(workPathActual, DisplayVersion); - CachePathBackup = Path.Combine(workPathBackup, DisplayVersion); + CachePathActual = Path.Combine(workPathActual, DisplayVersion) + "-" + cacheSelector; + CachePathBackup = Path.Combine(workPathBackup, DisplayVersion) + "-" + cacheSelector; PreLoad(); Profiler.PreLoad(); @@ -705,6 +709,10 @@ namespace ARMeilleure.Translation.PTC { imm = translator.Stubs.DispatchStub; } + else if (symbol == FunctionTableSymbol) + { + imm = translator.FunctionTable.Base; + } if (imm == null) { @@ -848,18 +856,15 @@ namespace ARMeilleure.Translation.PTC } } - List threads = new(); - for (int i = 0; i < degreeOfParallelism; i++) - { - Thread thread = new(TranslateFuncs) - { - IsBackground = true, - Name = "Ptc.TranslateThread." + i - }; - - threads.Add(thread); - } + List threads = Enumerable.Range(0, degreeOfParallelism) + .Select(idx => + new Thread(TranslateFuncs) + { + IsBackground = true, + Name = "Ptc.TranslateThread." + idx + } + ).ToList(); Stopwatch sw = Stopwatch.StartNew(); diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs index 24fbd7621..162368782 100644 --- a/src/ARMeilleure/Translation/Translator.cs +++ b/src/ARMeilleure/Translation/Translator.cs @@ -22,33 +22,13 @@ namespace ARMeilleure.Translation { public class Translator { - private static readonly AddressTable.Level[] _levels64Bit = - new AddressTable.Level[] - { - new(31, 17), - new(23, 8), - new(15, 8), - new( 7, 8), - new( 2, 5), - }; - - private static readonly AddressTable.Level[] _levels32Bit = - new AddressTable.Level[] - { - new(31, 17), - new(23, 8), - new(15, 8), - new( 7, 8), - new( 1, 6), - }; - private readonly IJitMemoryAllocator _allocator; private readonly ConcurrentQueue> _oldFuncs; private readonly Ptc _ptc; internal TranslatorCache Functions { get; } - internal AddressTable FunctionTable { get; } + internal IAddressTable FunctionTable { get; } internal EntryTable CountTable { get; } internal TranslatorStubs Stubs { get; } internal TranslatorQueue Queue { get; } @@ -57,7 +37,7 @@ namespace ARMeilleure.Translation private Thread[] _backgroundTranslationThreads; private volatile int _threadCount; - public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, bool for64Bits) + public Translator(IJitMemoryAllocator allocator, IMemoryManager memory, IAddressTable functionTable) { _allocator = allocator; Memory = memory; @@ -72,15 +52,15 @@ namespace ARMeilleure.Translation CountTable = new EntryTable(); Functions = new TranslatorCache(); - FunctionTable = new AddressTable(for64Bits ? _levels64Bit : _levels32Bit); + FunctionTable = functionTable; Stubs = new TranslatorStubs(FunctionTable); FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub; } - public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled) + public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) { - _ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type); + _ptc.Initialize(titleIdText, displayVersion, enabled, Memory.Type, cacheSelector); return _ptc; } diff --git a/src/ARMeilleure/Translation/TranslatorStubs.cs b/src/ARMeilleure/Translation/TranslatorStubs.cs index 364cca13c..bd9aed8d4 100644 --- a/src/ARMeilleure/Translation/TranslatorStubs.cs +++ b/src/ARMeilleure/Translation/TranslatorStubs.cs @@ -19,7 +19,7 @@ namespace ARMeilleure.Translation private bool _disposed; - private readonly AddressTable _functionTable; + private readonly IAddressTable _functionTable; private readonly Lazy _dispatchStub; private readonly Lazy _dispatchLoop; private readonly Lazy _contextWrapper; @@ -86,7 +86,7 @@ namespace ARMeilleure.Translation /// /// Function table used to store pointers to the functions that the guest code will call /// is null - public TranslatorStubs(AddressTable functionTable) + public TranslatorStubs(IAddressTable functionTable) { ArgumentNullException.ThrowIfNull(functionTable); diff --git a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj index b5fd8f9e7..bdf46d688 100644 --- a/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj +++ b/src/Ryujinx.Audio.Backends.OpenAL/Ryujinx.Audio.Backends.OpenAL.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj index dd18e70a1..940e47308 100644 --- a/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj +++ b/src/Ryujinx.Audio.Backends.SDL2/Ryujinx.Audio.Backends.SDL2.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj index 5c9423463..671a6ad5e 100644 --- a/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj +++ b/src/Ryujinx.Audio.Backends.SoundIo/Ryujinx.Audio.Backends.SoundIo.csproj @@ -4,6 +4,7 @@ net8.0 true win-x64;osx-x64;linux-x64 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Audio/Ryujinx.Audio.csproj b/src/Ryujinx.Audio/Ryujinx.Audio.csproj index fc20f4ec4..8901bbf59 100644 --- a/src/Ryujinx.Audio/Ryujinx.Audio.csproj +++ b/src/Ryujinx.Audio/Ryujinx.Audio.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index 0cb49ca8c..6b8152b9d 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -2,7 +2,7 @@ namespace Ryujinx.Common.Configuration.Hid { public class KeyboardHotkeys { - public Key ToggleVsync { get; set; } + public Key ToggleVSyncMode { get; set; } public Key Screenshot { get; set; } public Key ShowUI { get; set; } public Key Pause { get; set; } @@ -11,5 +11,7 @@ namespace Ryujinx.Common.Configuration.Hid public Key ResScaleDown { get; set; } public Key VolumeUp { get; set; } public Key VolumeDown { get; set; } + public Key CustomVSyncIntervalIncrement { get; set; } + public Key CustomVSyncIntervalDecrement { get; set; } } } diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs index 69f7d876d..be0e1518c 100644 --- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs +++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs @@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer public enum MultiplayerMode { Disabled, + LdnRyu, LdnMitm, } } diff --git a/src/Ryujinx.Common/Configuration/VSyncMode.cs b/src/Ryujinx.Common/Configuration/VSyncMode.cs new file mode 100644 index 000000000..ca93b5e1c --- /dev/null +++ b/src/Ryujinx.Common/Configuration/VSyncMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Common.Configuration +{ + public enum VSyncMode + { + Switch, + Unbounded, + Custom + } +} diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index 1b404a06a..a4117580e 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging TamperMachine, UI, Vic, + XCIFileTrimmer } } diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs index 02c6dc97b..a9dbe646a 100644 --- a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs +++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs @@ -30,10 +30,10 @@ namespace Ryujinx.Common.Logging.Targets string ILogTarget.Name { get => _target.Name; } public AsyncLogTargetWrapper(ILogTarget target) - : this(target, -1, AsyncLogTargetOverflowAction.Block) + : this(target, -1) { } - public AsyncLogTargetWrapper(ILogTarget target, int queueLimit, AsyncLogTargetOverflowAction overflowAction) + public AsyncLogTargetWrapper(ILogTarget target, int queueLimit = -1, AsyncLogTargetOverflowAction overflowAction = AsyncLogTargetOverflowAction.Block) { _target = target; _messageQueue = new BlockingCollection(queueLimit); diff --git a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs index 8d4ede96c..94e9359c8 100644 --- a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs +++ b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs @@ -47,7 +47,7 @@ namespace Ryujinx.Common.Logging.Targets } // Clean up old logs, should only keep 3 - FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray(); + FileInfo[] files = logDir.GetFiles("*.log").OrderBy(info => info.CreationTime).ToArray(); for (int i = 0; i < files.Length - 2; i++) { try @@ -69,9 +69,10 @@ namespace Ryujinx.Common.Logging.Targets } string version = ReleaseInformation.Version; + string appName = ReleaseInformation.IsCanaryBuild ? "Ryujinx_Canary" : "Ryujinx"; // Get path for the current time - path = Path.Combine(logDir.FullName, $"Ryujinx_{version}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); + path = Path.Combine(logDir.FullName, $"{appName}_{version}_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); try { diff --git a/src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs b/src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs new file mode 100644 index 000000000..fb11432b0 --- /dev/null +++ b/src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs @@ -0,0 +1,30 @@ +using Ryujinx.Common.Utilities; + +namespace Ryujinx.Common.Logging +{ + public class XCIFileTrimmerLog : XCIFileTrimmer.ILog + { + public virtual void Progress(long current, long total, string text, bool complete) + { + } + + public void Write(XCIFileTrimmer.LogType logType, string text) + { + switch (logType) + { + case XCIFileTrimmer.LogType.Info: + Logger.Notice.Print(LogClass.XCIFileTrimmer, text); + break; + case XCIFileTrimmer.LogType.Warn: + Logger.Warning?.Print(LogClass.XCIFileTrimmer, text); + break; + case XCIFileTrimmer.LogType.Error: + Logger.Error?.Print(LogClass.XCIFileTrimmer, text); + break; + case XCIFileTrimmer.LogType.Progress: + Logger.Info?.Print(LogClass.XCIFileTrimmer, text); + break; + } + } + } +} diff --git a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs index 762c73889..fcb2229a7 100644 --- a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs +++ b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs @@ -803,18 +803,6 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); } - public struct Array256 : IArray where T : unmanaged - { - T _e0; - Array128 _other; - Array127 _other2; - public readonly int Length => 256; - public ref T this[int index] => ref AsSpan()[index]; - - [Pure] - public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); - } - public struct Array140 : IArray where T : unmanaged { T _e0; @@ -828,6 +816,18 @@ namespace Ryujinx.Common.Memory public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); } + public struct Array256 : IArray where T : unmanaged + { + T _e0; + Array128 _other; + Array127 _other2; + public readonly int Length => 256; + public ref T this[int index] => ref AsSpan()[index]; + + [Pure] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length); + } + public struct Array384 : IArray where T : unmanaged { T _e0; diff --git a/src/Ryujinx.Common/ReactiveObject.cs b/src/Ryujinx.Common/ReactiveObject.cs index 4f27af546..8df1e20fe 100644 --- a/src/Ryujinx.Common/ReactiveObject.cs +++ b/src/Ryujinx.Common/ReactiveObject.cs @@ -1,11 +1,13 @@ +using Ryujinx.Common.Logging; using System; +using System.Globalization; using System.Threading; namespace Ryujinx.Common { public class ReactiveObject { - private readonly ReaderWriterLockSlim _readerWriterLock = new(); + private readonly ReaderWriterLockSlim _rwLock = new(); private bool _isInitialized; private T _value; @@ -15,15 +17,15 @@ namespace Ryujinx.Common { get { - _readerWriterLock.EnterReadLock(); + _rwLock.EnterReadLock(); T value = _value; - _readerWriterLock.ExitReadLock(); + _rwLock.ExitReadLock(); return value; } set { - _readerWriterLock.EnterWriteLock(); + _rwLock.EnterWriteLock(); T oldValue = _value; @@ -32,7 +34,7 @@ namespace Ryujinx.Common _isInitialized = true; _value = value; - _readerWriterLock.ExitWriteLock(); + _rwLock.ExitWriteLock(); if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value)) { @@ -40,12 +42,22 @@ namespace Ryujinx.Common } } } + + public void LogChangesToValue(string valueName, LogClass logClass = LogClass.Configuration) + => Event += (_, e) => ReactiveObjectHelper.LogValueChange(logClass, e, valueName); public static implicit operator T(ReactiveObject obj) => obj.Value; } public static class ReactiveObjectHelper { + public static void LogValueChange(LogClass logClass, ReactiveEventArgs eventArgs, string valueName) + { + string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}"); + + Logger.Info?.Print(logClass, message); + } + public static void Toggle(this ReactiveObject rBoolean) => rBoolean.Value = !rBoolean.Value; } diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs index dfba64191..011d9848a 100644 --- a/src/Ryujinx.Common/ReleaseInformation.cs +++ b/src/Ryujinx.Common/ReleaseInformation.cs @@ -1,3 +1,4 @@ +using System; using System.Reflection; namespace Ryujinx.Common @@ -5,7 +6,9 @@ namespace Ryujinx.Common // DO NOT EDIT, filled by CI public static class ReleaseInformation { - private const string FlatHubChannelOwner = "flathub"; + private const string FlatHubChannel = "flathub"; + private const string CanaryChannel = "canary"; + private const string ReleaseChannel = "release"; private const string BuildVersion = "%%RYUJINX_BUILD_VERSION%%"; public const string BuildGitHash = "%%RYUJINX_BUILD_GIT_HASH%%"; @@ -13,6 +16,7 @@ namespace Ryujinx.Common private const string ConfigFileName = "%%RYUJINX_CONFIG_FILE_NAME%%"; public const string ReleaseChannelOwner = "%%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER%%"; + public const string ReleaseChannelSourceRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO%%"; public const string ReleaseChannelRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_REPO%%"; public static string ConfigName => !ConfigFileName.StartsWith("%%") ? ConfigFileName : "Config.json"; @@ -21,11 +25,24 @@ namespace Ryujinx.Common !BuildGitHash.StartsWith("%%") && !ReleaseChannelName.StartsWith("%%") && !ReleaseChannelOwner.StartsWith("%%") && + !ReleaseChannelSourceRepo.StartsWith("%%") && !ReleaseChannelRepo.StartsWith("%%") && !ConfigFileName.StartsWith("%%"); - public static bool IsFlatHubBuild => IsValid && ReleaseChannelOwner.Equals(FlatHubChannelOwner); + public static bool IsFlatHubBuild => IsValid && ReleaseChannelOwner.Equals(FlatHubChannel); + + public static bool IsCanaryBuild => IsValid && ReleaseChannelName.Equals(CanaryChannel); + + public static bool IsReleaseBuild => IsValid && ReleaseChannelName.Equals(ReleaseChannel); public static string Version => IsValid ? BuildVersion : Assembly.GetEntryAssembly()!.GetCustomAttribute()?.InformationalVersion; + + public static string GetChangelogUrl(Version currentVersion, Version newVersion) => + IsCanaryBuild + ? $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelSourceRepo}/compare/Canary-{currentVersion}...Canary-{newVersion}" + : $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelSourceRepo}/releases/tag/{newVersion}"; + + public static string GetChangelogForVersion(Version version) => + $"https://github.com/{ReleaseChannelOwner}/{ReleaseChannelRepo}/releases/tag/{version}"; } } diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj index dee462fdb..85d4b58bd 100644 --- a/src/Ryujinx.Common/Ryujinx.Common.csproj +++ b/src/Ryujinx.Common/Ryujinx.Common.csproj @@ -4,6 +4,7 @@ net8.0 true $(DefineConstants);$(ExtraDefineConstants) + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs index 71e02184e..53d1e4f33 100644 --- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs +++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs @@ -1,6 +1,7 @@ using System.Buffers.Binary; using System.Net; using System.Net.NetworkInformation; +using System.Runtime.InteropServices; namespace Ryujinx.Common.Utilities { @@ -65,6 +66,11 @@ namespace Ryujinx.Common.Utilities return (targetProperties, targetAddressInfo); } + public static bool SupportsDynamicDns() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + public static uint ConvertIpv4Address(IPAddress ipAddress) { return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes()); diff --git a/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs b/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs new file mode 100644 index 000000000..050e78d1e --- /dev/null +++ b/src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs @@ -0,0 +1,524 @@ +// Uncomment the line below to ensure XCIFileTrimmer does not modify files +//#define XCI_TRIMMER_READ_ONLY_MODE + +using Gommon; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Ryujinx.Common.Utilities +{ + public sealed class XCIFileTrimmer + { + private const long BytesInAMegabyte = 1024 * 1024; + private const int BufferSize = 8 * (int)BytesInAMegabyte; + + private const long CartSizeMBinFormattedGB = 952; + private const int CartKeyAreaSize = 0x1000; + private const byte PaddingByte = 0xFF; + private const int HeaderFilePos = 0x100; + private const int CartSizeFilePos = 0x10D; + private const int DataSizeFilePos = 0x118; + private const string HeaderMagicValue = "HEAD"; + + /// + /// Cartridge Sizes (ByteIdentifier, SizeInGB) + /// + private static readonly Dictionary _cartSizesGB = new() + { + { 0xFA, 1 }, + { 0xF8, 2 }, + { 0xF0, 4 }, + { 0xE0, 8 }, + { 0xE1, 16 }, + { 0xE2, 32 } + }; + + private static long RecordsToByte(long records) + { + return 512 + (records * 512); + } + + public static bool CanTrim(string filename, ILog log = null) + { + if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase)) + { + var trimmer = new XCIFileTrimmer(filename, log); + return trimmer.CanBeTrimmed; + } + + return false; + } + + public static bool CanUntrim(string filename, ILog log = null) + { + if (Path.GetExtension(filename).Equals(".XCI", StringComparison.InvariantCultureIgnoreCase)) + { + var trimmer = new XCIFileTrimmer(filename, log); + return trimmer.CanBeUntrimmed; + } + + return false; + } + + private ILog _log; + private string _filename; + private FileStream _fileStream; + private BinaryReader _binaryReader; + private long _offsetB, _dataSizeB, _cartSizeB, _fileSizeB; + private bool _fileOK = true; + private bool _freeSpaceChecked = false; + private bool _freeSpaceValid = false; + + public enum OperationOutcome + { + Undetermined, + InvalidXCIFile, + NoTrimNecessary, + NoUntrimPossible, + FreeSpaceCheckFailed, + FileIOWriteError, + ReadOnlyFileCannotFix, + FileSizeChanged, + Successful, + Cancelled + } + + public enum LogType + { + Info, + Warn, + Error, + Progress + } + + public interface ILog + { + public void Write(LogType logType, string text); + public void Progress(long current, long total, string text, bool complete); + } + + public bool FileOK => _fileOK; + public bool Trimmed => _fileOK && FileSizeB < UntrimmedFileSizeB; + public bool ContainsKeyArea => _offsetB != 0; + public bool CanBeTrimmed => _fileOK && FileSizeB > TrimmedFileSizeB; + public bool CanBeUntrimmed => _fileOK && FileSizeB < UntrimmedFileSizeB; + public bool FreeSpaceChecked => _fileOK && _freeSpaceChecked; + public bool FreeSpaceValid => _fileOK && _freeSpaceValid; + public long DataSizeB => _dataSizeB; + public long CartSizeB => _cartSizeB; + public long FileSizeB => _fileSizeB; + public long DiskSpaceSavedB => CartSizeB - FileSizeB; + public long DiskSpaceSavingsB => CartSizeB - DataSizeB; + public long TrimmedFileSizeB => _offsetB + _dataSizeB; + public long UntrimmedFileSizeB => _offsetB + _cartSizeB; + + public ILog Log + { + get => _log; + set => _log = value; + } + + public String Filename + { + get => _filename; + set + { + _filename = value; + Reset(); + } + } + + public long Pos + { + get => _fileStream.Position; + set => _fileStream.Position = value; + } + + public XCIFileTrimmer(string path, ILog log = null) + { + Log = log; + Filename = path; + ReadHeader(); + } + + public void CheckFreeSpace(CancellationToken? cancelToken = null) + { + if (FreeSpaceChecked) + return; + + try + { + if (CanBeTrimmed) + { + _freeSpaceValid = false; + + OpenReaders(); + + try + { + Pos = TrimmedFileSizeB; + bool freeSpaceValid = true; + long readSizeB = FileSizeB - TrimmedFileSizeB; + + Stopwatch timedSw = Lambda.Timed(() => + { + freeSpaceValid = CheckPadding(readSizeB, cancelToken); + }); + + if (timedSw.Elapsed.TotalSeconds > 0) + { + Log?.Write(LogType.Info, $"Checked at {readSizeB / (double)XCIFileTrimmer.BytesInAMegabyte / timedSw.Elapsed.TotalSeconds:N} Mb/sec"); + } + + if (freeSpaceValid) + Log?.Write(LogType.Info, "Free space is valid"); + + _freeSpaceValid = freeSpaceValid; + } + finally + { + CloseReaders(); + } + + } + else + { + Log?.Write(LogType.Warn, "There is no free space to check."); + _freeSpaceValid = false; + } + } + finally + { + _freeSpaceChecked = true; + } + } + + private bool CheckPadding(long readSizeB, CancellationToken? cancelToken = null) + { + long maxReads = readSizeB / XCIFileTrimmer.BufferSize; + long read = 0; + var buffer = new byte[BufferSize]; + + while (true) + { + if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested) + { + return false; + } + + int bytes = _fileStream.Read(buffer, 0, XCIFileTrimmer.BufferSize); + if (bytes == 0) + break; + + Log?.Progress(read, maxReads, "Verifying file can be trimmed", false); + if (buffer.Take(bytes).AsParallel().Any(b => b != XCIFileTrimmer.PaddingByte)) + { + Log?.Write(LogType.Warn, "Free space is NOT valid"); + return false; + } + + read++; + } + + return true; + } + + private void Reset() + { + _freeSpaceChecked = false; + _freeSpaceValid = false; + ReadHeader(); + } + + public OperationOutcome Trim(CancellationToken? cancelToken = null) + { + if (!FileOK) + { + return OperationOutcome.InvalidXCIFile; + } + + if (!CanBeTrimmed) + { + return OperationOutcome.NoTrimNecessary; + } + + if (!FreeSpaceChecked) + { + CheckFreeSpace(cancelToken); + } + + if (!FreeSpaceValid) + { + if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested) + { + return OperationOutcome.Cancelled; + } + else + { + return OperationOutcome.FreeSpaceCheckFailed; + } + } + + Log?.Write(LogType.Info, "Trimming..."); + + try + { + var info = new FileInfo(Filename); + if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + try + { + Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute"); + File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly); + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.ReadOnlyFileCannotFix; + } + } + + if (info.Length != FileSizeB) + { + Log?.Write(LogType.Error, "File size has changed, cannot safely trim."); + return OperationOutcome.FileSizeChanged; + } + + var outfileStream = new FileStream(_filename, FileMode.Open, FileAccess.Write, FileShare.Write); + + try + { + +#if !XCI_TRIMMER_READ_ONLY_MODE + outfileStream.SetLength(TrimmedFileSizeB); +#endif + return OperationOutcome.Successful; + } + finally + { + outfileStream.Close(); + Reset(); + } + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.FileIOWriteError; + } + } + + public OperationOutcome Untrim(CancellationToken? cancelToken = null) + { + if (!FileOK) + { + return OperationOutcome.InvalidXCIFile; + } + + if (!CanBeUntrimmed) + { + return OperationOutcome.NoUntrimPossible; + } + + try + { + Log?.Write(LogType.Info, "Untrimming..."); + + var info = new FileInfo(Filename); + if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + try + { + Log?.Write(LogType.Info, "Attempting to remove ReadOnly attribute"); + File.SetAttributes(Filename, info.Attributes & ~FileAttributes.ReadOnly); + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.ReadOnlyFileCannotFix; + } + } + + if (info.Length != FileSizeB) + { + Log?.Write(LogType.Error, "File size has changed, cannot safely untrim."); + return OperationOutcome.FileSizeChanged; + } + + var outfileStream = new FileStream(_filename, FileMode.Append, FileAccess.Write, FileShare.Write); + long bytesToWriteB = UntrimmedFileSizeB - FileSizeB; + + try + { + Stopwatch timedSw = Lambda.Timed(() => + { + WritePadding(outfileStream, bytesToWriteB, cancelToken); + }); + + if (timedSw.Elapsed.TotalSeconds > 0) + { + Log?.Write(LogType.Info, $"Wrote at {bytesToWriteB / (double)XCIFileTrimmer.BytesInAMegabyte / timedSw.Elapsed.TotalSeconds:N} Mb/sec"); + } + + if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested) + { + return OperationOutcome.Cancelled; + } + else + { + return OperationOutcome.Successful; + } + } + finally + { + outfileStream.Close(); + Reset(); + } + } + catch (Exception e) + { + Log?.Write(LogType.Error, e.ToString()); + return OperationOutcome.FileIOWriteError; + } + } + + private void WritePadding(FileStream outfileStream, long bytesToWriteB, CancellationToken? cancelToken = null) + { + long bytesLeftToWriteB = bytesToWriteB; + long writes = bytesLeftToWriteB / XCIFileTrimmer.BufferSize; + int write = 0; + + try + { + var buffer = new byte[BufferSize]; + Array.Fill(buffer, XCIFileTrimmer.PaddingByte); + + while (bytesLeftToWriteB > 0) + { + if (cancelToken.HasValue && cancelToken.Value.IsCancellationRequested) + { + return; + } + + long bytesToWrite = Math.Min(XCIFileTrimmer.BufferSize, bytesLeftToWriteB); + +#if !XCI_TRIMMER_READ_ONLY_MODE + outfileStream.Write(buffer, 0, (int)bytesToWrite); +#endif + + bytesLeftToWriteB -= bytesToWrite; + Log?.Progress(write, writes, "Writing padding data...", false); + write++; + } + } + finally + { + Log?.Progress(write, writes, "Writing padding data...", true); + } + } + + private void OpenReaders() + { + if (_binaryReader == null) + { + _fileStream = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read); + _binaryReader = new BinaryReader(_fileStream); + } + } + + private void CloseReaders() + { + if (_binaryReader != null && _binaryReader.BaseStream != null) + _binaryReader.Close(); + _binaryReader = null; + _fileStream = null; + GC.Collect(); + } + + private void ReadHeader() + { + try + { + OpenReaders(); + + try + { + // Attempt without key area + bool success = CheckAndReadHeader(false); + + if (!success) + { + // Attempt with key area + success = CheckAndReadHeader(true); + } + + _fileOK = success; + } + finally + { + CloseReaders(); + } + } + catch (Exception ex) + { + Log?.Write(LogType.Error, ex.Message); + _fileOK = false; + _dataSizeB = 0; + _cartSizeB = 0; + _fileSizeB = 0; + _offsetB = 0; + } + } + + private bool CheckAndReadHeader(bool assumeKeyArea) + { + // Read file size + _fileSizeB = _fileStream.Length; + if (_fileSizeB < 32 * 1024) + { + Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the data size is too small"); + return false; + } + + // Setup offset + _offsetB = (long)(assumeKeyArea ? XCIFileTrimmer.CartKeyAreaSize : 0); + + // Check header + Pos = _offsetB + XCIFileTrimmer.HeaderFilePos; + string head = System.Text.Encoding.ASCII.GetString(_binaryReader.ReadBytes(4)); + if (head != XCIFileTrimmer.HeaderMagicValue) + { + if (!assumeKeyArea) + { + Log?.Write(LogType.Warn, $"Incorrect header found, file mat contain a key area..."); + } + else + { + Log?.Write(LogType.Error, "The source file doesn't look like an XCI file as the header is corrupted"); + } + + return false; + } + + // Read Cart Size + Pos = _offsetB + XCIFileTrimmer.CartSizeFilePos; + byte cartSizeId = _binaryReader.ReadByte(); + if (!_cartSizesGB.TryGetValue(cartSizeId, out long cartSizeNGB)) + { + Log?.Write(LogType.Error, $"The source file doesn't look like an XCI file as the Cartridge Size is incorrect (0x{cartSizeId:X2})"); + return false; + } + _cartSizeB = cartSizeNGB * XCIFileTrimmer.CartSizeMBinFormattedGB * XCIFileTrimmer.BytesInAMegabyte; + + // Read data size + Pos = _offsetB + XCIFileTrimmer.DataSizeFilePos; + long records = (long)BitConverter.ToUInt32(_binaryReader.ReadBytes(4), 0); + _dataSizeB = RecordsToByte(records); + + return true; + } + } +} diff --git a/src/Ryujinx.Cpu/AddressTable.cs b/src/Ryujinx.Cpu/AddressTable.cs new file mode 100644 index 000000000..038a2009c --- /dev/null +++ b/src/Ryujinx.Cpu/AddressTable.cs @@ -0,0 +1,482 @@ +using ARMeilleure.Memory; +using Ryujinx.Common; +using Ryujinx.Cpu.Signal; +using Ryujinx.Memory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using static Ryujinx.Cpu.MemoryEhMeilleure; + +namespace ARMeilleure.Common +{ + /// + /// Represents a table of guest address to a value. + /// + /// Type of the value + public unsafe class AddressTable : IAddressTable where TEntry : unmanaged + { + /// + /// Represents a page of the address table. + /// + private readonly struct AddressTablePage + { + /// + /// True if the allocation belongs to a sparse block, false otherwise. + /// + public readonly bool IsSparse; + + /// + /// Base address for the page. + /// + public readonly IntPtr Address; + + public AddressTablePage(bool isSparse, IntPtr address) + { + IsSparse = isSparse; + Address = address; + } + } + + /// + /// A sparsely mapped block of memory with a signal handler to map pages as they're accessed. + /// + private readonly struct TableSparseBlock : IDisposable + { + public readonly SparseMemoryBlock Block; + private readonly TrackingEventDelegate _trackingEvent; + + public TableSparseBlock(ulong size, Action ensureMapped, PageInitDelegate pageInit) + { + var block = new SparseMemoryBlock(size, pageInit, null); + + _trackingEvent = (ulong address, ulong size, bool write) => + { + ulong pointer = (ulong)block.Block.Pointer + address; + ensureMapped((IntPtr)pointer); + return pointer; + }; + + bool added = NativeSignalHandler.AddTrackedRegion( + (nuint)block.Block.Pointer, + (nuint)(block.Block.Pointer + (IntPtr)block.Block.Size), + Marshal.GetFunctionPointerForDelegate(_trackingEvent)); + + if (!added) + { + throw new InvalidOperationException("Number of allowed tracked regions exceeded."); + } + + Block = block; + } + + public void Dispose() + { + NativeSignalHandler.RemoveTrackedRegion((nuint)Block.Block.Pointer); + + Block.Dispose(); + } + } + + private bool _disposed; + private TEntry** _table; + private readonly List _pages; + private TEntry _fill; + + private readonly MemoryBlock _sparseFill; + private readonly SparseMemoryBlock _fillBottomLevel; + private readonly TEntry* _fillBottomLevelPtr; + + private readonly List _sparseReserved; + private readonly ReaderWriterLockSlim _sparseLock; + + private ulong _sparseBlockSize; + private ulong _sparseReservedOffset; + + public bool Sparse { get; } + + /// + public ulong Mask { get; } + + /// + public AddressTableLevel[] Levels { get; } + + /// + public TEntry Fill + { + get + { + return _fill; + } + set + { + UpdateFill(value); + } + } + + /// + public IntPtr Base + { + get + { + ObjectDisposedException.ThrowIf(_disposed, this); + + lock (_pages) + { + return (IntPtr)GetRootPage(); + } + } + } + + /// + /// Constructs a new instance of the class with the specified list of + /// . + /// + /// Levels for the address table + /// True if the bottom page should be sparsely mapped + /// is null + /// Length of is less than 2 + public AddressTable(AddressTableLevel[] levels, bool sparse) + { + ArgumentNullException.ThrowIfNull(levels); + + _pages = new List(capacity: 16); + + Levels = levels; + Mask = 0; + + foreach (var level in Levels) + { + Mask |= level.Mask; + } + + Sparse = sparse; + + if (sparse) + { + // If the address table is sparse, allocate a fill block + + _sparseFill = new MemoryBlock(268435456ul, MemoryAllocationFlags.Mirrorable); //low Power TC uses size: 65536ul + + ulong bottomLevelSize = (1ul << levels.Last().Length) * (ulong)sizeof(TEntry); + + _fillBottomLevel = new SparseMemoryBlock(bottomLevelSize, null, _sparseFill); + _fillBottomLevelPtr = (TEntry*)_fillBottomLevel.Block.Pointer; + + _sparseReserved = new List(); + _sparseLock = new ReaderWriterLockSlim(); + + _sparseBlockSize = bottomLevelSize; + } + } + + /// + /// Create an instance for an ARM function table. + /// Selects the best table structure for A32/A64, taking into account the selected memory manager type. + /// + /// True if the guest is A64, false otherwise + /// Memory manager type + /// An for ARM function lookup + public static AddressTable CreateForArm(bool for64Bits, MemoryManagerType type) + { + // Assume software memory means that we don't want to use any signal handlers. + bool sparse = type != MemoryManagerType.SoftwareMmu && type != MemoryManagerType.SoftwarePageTable; + + return new AddressTable(AddressTablePresets.GetArmPreset(for64Bits, sparse), sparse); + } + + /// + /// Update the fill value for the bottom level of the table. + /// + /// New fill value + private void UpdateFill(TEntry fillValue) + { + if (_sparseFill != null) + { + Span span = _sparseFill.GetSpan(0, (int)_sparseFill.Size); + MemoryMarshal.Cast(span).Fill(fillValue); + } + + _fill = fillValue; + } + + /// + /// Signal that the given code range exists. + /// + /// + /// + public void SignalCodeRange(ulong address, ulong size) + { + AddressTableLevel bottom = Levels.Last(); + ulong bottomLevelEntries = 1ul << bottom.Length; + + ulong entryIndex = address >> bottom.Index; + ulong entries = size >> bottom.Index; + entries += entryIndex - BitUtils.AlignDown(entryIndex, bottomLevelEntries); + + _sparseBlockSize = Math.Max(_sparseBlockSize, BitUtils.AlignUp(entries, bottomLevelEntries) * (ulong)sizeof(TEntry)); + } + + /// + public bool IsValid(ulong address) + { + return (address & ~Mask) == 0; + } + + /// + public ref TEntry GetValue(ulong address) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (!IsValid(address)) + { + throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address)); + } + + lock (_pages) + { + TEntry* page = GetPage(address); + + long index = Levels[^1].GetValue(address); + + EnsureMapped((IntPtr)(page + index)); + + return ref page[index]; + } + } + + /// + /// Gets the leaf page for the specified guest . + /// + /// Guest address + /// Leaf page for the specified guest + private TEntry* GetPage(ulong address) + { + TEntry** page = GetRootPage(); + + for (int i = 0; i < Levels.Length - 1; i++) + { + ref AddressTableLevel level = ref Levels[i]; + ref TEntry* nextPage = ref page[level.GetValue(address)]; + + if (nextPage == null || nextPage == _fillBottomLevelPtr) + { + ref AddressTableLevel nextLevel = ref Levels[i + 1]; + + if (i == Levels.Length - 2) + { + nextPage = (TEntry*)Allocate(1 << nextLevel.Length, Fill, leaf: true); + } + else + { + nextPage = (TEntry*)Allocate(1 << nextLevel.Length, GetFillValue(i), leaf: false); + } + } + + page = (TEntry**)nextPage; + } + + return (TEntry*)page; + } + + /// + /// Ensure the given pointer is mapped in any overlapping sparse reservations. + /// + /// Pointer to be mapped + private void EnsureMapped(IntPtr ptr) + { + if (Sparse) + { + // Check sparse allocations to see if the pointer is in any of them. + // Ensure the page is committed if there's a match. + + _sparseLock.EnterReadLock(); + + try + { + foreach (TableSparseBlock reserved in _sparseReserved) + { + SparseMemoryBlock sparse = reserved.Block; + + if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size) + { + sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer)); + + break; + } + } + } + finally + { + _sparseLock.ExitReadLock(); + } + } + } + + /// + /// Get the fill value for a non-leaf level of the table. + /// + /// Level to get the fill value for + /// The fill value + private IntPtr GetFillValue(int level) + { + if (_fillBottomLevel != null && level == Levels.Length - 2) + { + return (IntPtr)_fillBottomLevelPtr; + } + else + { + return IntPtr.Zero; + } + } + + /// + /// Lazily initialize and get the root page of the . + /// + /// Root page of the + private TEntry** GetRootPage() + { + if (_table == null) + { + if (Levels.Length == 1) + _table = (TEntry**)Allocate(1 << Levels[0].Length, Fill, leaf: true); + else + _table = (TEntry**)Allocate(1 << Levels[0].Length, GetFillValue(0), leaf: false); + } + + return _table; + } + + /// + /// Initialize a leaf page with the fill value. + /// + /// Page to initialize + private void InitLeafPage(Span page) + { + MemoryMarshal.Cast(page).Fill(_fill); + } + + /// + /// Reserve a new sparse block, and add it to the list. + /// + /// The new sparse block that was added + private TableSparseBlock ReserveNewSparseBlock() + { + var block = new TableSparseBlock(_sparseBlockSize, EnsureMapped, InitLeafPage); + + _sparseReserved.Add(block); + _sparseReservedOffset = 0; + + return block; + } + + /// + /// Allocates a block of memory of the specified type and length. + /// + /// Type of elements + /// Number of elements + /// Fill value + /// if leaf; otherwise + /// Allocated block + private IntPtr Allocate(int length, T fill, bool leaf) where T : unmanaged + { + var size = sizeof(T) * length; + + AddressTablePage page; + + if (Sparse && leaf) + { + _sparseLock.EnterWriteLock(); + + SparseMemoryBlock block; + + if (_sparseReserved.Count == 0) + { + block = ReserveNewSparseBlock().Block; + } + else + { + block = _sparseReserved.Last().Block; + + if (_sparseReservedOffset == block.Block.Size) + { + block = ReserveNewSparseBlock().Block; + } + } + + page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset); + + _sparseReservedOffset += (ulong)size; + + _sparseLock.ExitWriteLock(); + } + else + { + var address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size); + page = new AddressTablePage(false, address); + + var span = new Span((void*)page.Address, length); + span.Fill(fill); + } + + _pages.Add(page); + + //TranslatorEventSource.Log.AddressTableAllocated(size, leaf); + + return page.Address; + } + + /// + /// Releases all resources used by the instance. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases all unmanaged and optionally managed resources used by the + /// instance. + /// + /// to dispose managed resources also; otherwise just unmanaged resouces + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + foreach (var page in _pages) + { + if (!page.IsSparse) + { + Marshal.FreeHGlobal(page.Address); + } + } + + if (Sparse) + { + foreach (TableSparseBlock block in _sparseReserved) + { + block.Dispose(); + } + + _sparseReserved.Clear(); + + _fillBottomLevel.Dispose(); + _sparseFill.Dispose(); + _sparseLock.Dispose(); + } + + _disposed = true; + } + } + + /// + /// Frees resources used by the instance. + /// + ~AddressTable() + { + Dispose(false); + } + } +} diff --git a/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs b/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs index 99e4c0479..784949441 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvCpuContext.cs @@ -32,7 +32,7 @@ namespace Ryujinx.Cpu.AppleHv { } - public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled) + public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) { return new DummyDiskCacheLoadState(); } diff --git a/src/Ryujinx.Cpu/ICpuContext.cs b/src/Ryujinx.Cpu/ICpuContext.cs index edcebdfc4..1fb3b674d 100644 --- a/src/Ryujinx.Cpu/ICpuContext.cs +++ b/src/Ryujinx.Cpu/ICpuContext.cs @@ -48,7 +48,7 @@ namespace Ryujinx.Cpu /// Version of the application /// True if the cache should be loaded from disk if it exists, false otherwise /// Disk cache load progress reporter and manager - IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled); + IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector); /// /// Indicates that code has been loaded into guest memory, and that it might be executed in the future. diff --git a/src/Ryujinx.Cpu/Jit/JitCpuContext.cs b/src/Ryujinx.Cpu/Jit/JitCpuContext.cs index 9893c59b2..0793f382d 100644 --- a/src/Ryujinx.Cpu/Jit/JitCpuContext.cs +++ b/src/Ryujinx.Cpu/Jit/JitCpuContext.cs @@ -1,3 +1,4 @@ +using ARMeilleure.Common; using ARMeilleure.Memory; using ARMeilleure.Translation; using Ryujinx.Cpu.Signal; @@ -9,11 +10,13 @@ namespace Ryujinx.Cpu.Jit { private readonly ITickSource _tickSource; private readonly Translator _translator; + private readonly AddressTable _functionTable; public JitCpuContext(ITickSource tickSource, IMemoryManager memory, bool for64Bit) { _tickSource = tickSource; - _translator = new Translator(new JitMemoryAllocator(forJit: true), memory, for64Bit); + _functionTable = AddressTable.CreateForArm(for64Bit, memory.Type); + _translator = new Translator(new JitMemoryAllocator(forJit: true), memory, _functionTable); if (memory.Type.IsHostMappedOrTracked()) { @@ -47,14 +50,15 @@ namespace Ryujinx.Cpu.Jit } /// - public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled) + public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) { - return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled)); + return new JitDiskCacheLoadState(_translator.LoadDiskCache(titleIdText, displayVersion, enabled, cacheSelector)); } /// public void PrepareCodeRange(ulong address, ulong size) { + _functionTable.SignalCodeRange(address, size); _translator.PrepareCodeRange(address, size); } diff --git a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitFlow.cs b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitFlow.cs index 7f5e4835c..48bdbb573 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitFlow.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm32/Target/Arm64/InstEmitFlow.cs @@ -140,6 +140,10 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 bool isTail = false) { int tempRegister; + int tempGuestAddress = -1; + + bool inlineLookup = guestAddress.Kind != OperandKind.Constant && + funcTable is { Sparse: true }; if (guestAddress.Kind == OperandKind.Constant) { @@ -153,9 +157,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 else { asm.StrRiUn(guestAddress, Register(regAlloc.FixedContextRegister), NativeContextOffsets.DispatchAddressOffset); + + if (inlineLookup && guestAddress.Value == 0) + { + // X0 will be overwritten. Move the address to a temp register. + tempGuestAddress = regAlloc.AllocateTempGprRegister(); + asm.Mov(Register(tempGuestAddress), guestAddress); + } } - tempRegister = regAlloc.FixedContextRegister == 1 ? 2 : 1; + tempRegister = NextFreeRegister(1, tempGuestAddress); if (!isTail) { @@ -176,6 +187,40 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 asm.Mov(rn, funcPtrLoc & ~0xfffUL); asm.LdrRiUn(rn, rn, (int)(funcPtrLoc & 0xfffUL)); } + else if (inlineLookup) + { + // Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels. + + Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress)); + + if (tempGuestAddress != -1) + { + guestAddress = Register(tempGuestAddress); + } + + ulong tableBase = (ulong)funcTable.Base; + + // Index into the table. + asm.Mov(rn, tableBase); + + for (int i = 0; i < funcTable.Levels.Length; i++) + { + var level = funcTable.Levels[i]; + asm.Ubfx(indexReg, guestAddress, level.Index, level.Length); + asm.Lsl(indexReg, indexReg, Const(3)); + + // Index into the page. + asm.Add(rn, rn, indexReg); + + // Load the page address. + asm.LdrRiUn(rn, rn, 0); + } + + if (tempGuestAddress != -1) + { + regAlloc.FreeTempGprRegister(tempGuestAddress); + } + } else { asm.Mov(rn, (ulong)funcPtr); @@ -252,5 +297,20 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64 { return new Operand(register, RegisterType.Integer, type); } + + private static Operand Const(long value, OperandType type = OperandType.I64) + { + return new Operand(type, (ulong)value); + } + + private static int NextFreeRegister(int start, int avoid) + { + if (start == avoid) + { + start++; + } + + return start; + } } } diff --git a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs index 1eeeb746e..f534e8b6e 100644 --- a/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs +++ b/src/Ryujinx.Cpu/LightningJit/Arm64/Target/Arm64/InstEmitSystem.cs @@ -305,6 +305,10 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 bool isTail = false) { int tempRegister; + int tempGuestAddress = -1; + + bool inlineLookup = guestAddress.Kind != OperandKind.Constant && + funcTable is { Sparse: true }; if (guestAddress.Kind == OperandKind.Constant) { @@ -318,9 +322,16 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 else { asm.StrRiUn(guestAddress, Register(regAlloc.FixedContextRegister), NativeContextOffsets.DispatchAddressOffset); + + if (inlineLookup && guestAddress.Value == 0) + { + // X0 will be overwritten. Move the address to a temp register. + tempGuestAddress = regAlloc.AllocateTempGprRegister(); + asm.Mov(Register(tempGuestAddress), guestAddress); + } } - tempRegister = regAlloc.FixedContextRegister == 1 ? 2 : 1; + tempRegister = NextFreeRegister(1, tempGuestAddress); if (!isTail) { @@ -341,6 +352,40 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 asm.Mov(rn, funcPtrLoc & ~0xfffUL); asm.LdrRiUn(rn, rn, (int)(funcPtrLoc & 0xfffUL)); } + else if (inlineLookup) + { + // Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels. + + Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress)); + + if (tempGuestAddress != -1) + { + guestAddress = Register(tempGuestAddress); + } + + ulong tableBase = (ulong)funcTable.Base; + + // Index into the table. + asm.Mov(rn, tableBase); + + for (int i = 0; i < funcTable.Levels.Length; i++) + { + var level = funcTable.Levels[i]; + asm.Ubfx(indexReg, guestAddress, level.Index, level.Length); + asm.Lsl(indexReg, indexReg, Const(3)); + + // Index into the page. + asm.Add(rn, rn, indexReg); + + // Load the page address. + asm.LdrRiUn(rn, rn, 0); + } + + if (tempGuestAddress != -1) + { + regAlloc.FreeTempGprRegister(tempGuestAddress); + } + } else { asm.Mov(rn, (ulong)funcPtr); @@ -613,5 +658,20 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64 { return new Operand(register, RegisterType.Integer, type); } + + private static Operand Const(long value, OperandType type = OperandType.I64) + { + return new Operand(type, (ulong)value); + } + + private static int NextFreeRegister(int start, int avoid) + { + if (start == avoid) + { + start++; + } + + return start; + } } } diff --git a/src/Ryujinx.Cpu/LightningJit/LightningJitCpuContext.cs b/src/Ryujinx.Cpu/LightningJit/LightningJitCpuContext.cs index b63636e39..0f47ffb15 100644 --- a/src/Ryujinx.Cpu/LightningJit/LightningJitCpuContext.cs +++ b/src/Ryujinx.Cpu/LightningJit/LightningJitCpuContext.cs @@ -1,3 +1,4 @@ +using ARMeilleure.Common; using ARMeilleure.Memory; using Ryujinx.Cpu.Jit; using Ryujinx.Cpu.LightningJit.State; @@ -8,11 +9,16 @@ namespace Ryujinx.Cpu.LightningJit { private readonly ITickSource _tickSource; private readonly Translator _translator; + private readonly AddressTable _functionTable; public LightningJitCpuContext(ITickSource tickSource, IMemoryManager memory, bool for64Bit) { _tickSource = tickSource; - _translator = new Translator(memory, for64Bit); + + _functionTable = AddressTable.CreateForArm(for64Bit, memory.Type); + + _translator = new Translator(memory, _functionTable); + memory.UnmapEvent += UnmapHandler; } @@ -40,7 +46,7 @@ namespace Ryujinx.Cpu.LightningJit } /// - public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled) + public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector) { return new DummyDiskCacheLoadState(); } @@ -48,6 +54,7 @@ namespace Ryujinx.Cpu.LightningJit /// public void PrepareCodeRange(ulong address, ulong size) { + _functionTable.SignalCodeRange(address, size); } public void Dispose() diff --git a/src/Ryujinx.Cpu/LightningJit/Translator.cs b/src/Ryujinx.Cpu/LightningJit/Translator.cs index b4710e34e..4c4011f11 100644 --- a/src/Ryujinx.Cpu/LightningJit/Translator.cs +++ b/src/Ryujinx.Cpu/LightningJit/Translator.cs @@ -19,25 +19,6 @@ namespace Ryujinx.Cpu.LightningJit // Should be enabled on platforms that enforce W^X. private static bool IsNoWxPlatform => false; - private static readonly AddressTable.Level[] _levels64Bit = - new AddressTable.Level[] - { - new(31, 17), - new(23, 8), - new(15, 8), - new( 7, 8), - new( 2, 5), - }; - - private static readonly AddressTable.Level[] _levels32Bit = - new AddressTable.Level[] - { - new(23, 9), - new(15, 8), - new( 7, 8), - new( 1, 6), - }; - private readonly ConcurrentQueue> _oldFuncs; private readonly NoWxCache _noWxCache; private bool _disposed; @@ -47,7 +28,7 @@ namespace Ryujinx.Cpu.LightningJit internal TranslatorStubs Stubs { get; } internal IMemoryManager Memory { get; } - public Translator(IMemoryManager memory, bool for64Bits) + public Translator(IMemoryManager memory, AddressTable functionTable) { Memory = memory; @@ -63,7 +44,7 @@ namespace Ryujinx.Cpu.LightningJit } Functions = new TranslatorCache(); - FunctionTable = new AddressTable(for64Bits ? _levels64Bit : _levels32Bit); + FunctionTable = functionTable; Stubs = new TranslatorStubs(FunctionTable, _noWxCache); FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub; diff --git a/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs b/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs index e88414d5e..c5231e506 100644 --- a/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs +++ b/src/Ryujinx.Cpu/LightningJit/TranslatorStubs.cs @@ -23,7 +23,7 @@ namespace Ryujinx.Cpu.LightningJit private bool _disposed; - private readonly AddressTable _functionTable; + private readonly IAddressTable _functionTable; private readonly NoWxCache _noWxCache; private readonly GetFunctionAddressDelegate _getFunctionAddressRef; private readonly nint _getFunctionAddress; @@ -79,7 +79,7 @@ namespace Ryujinx.Cpu.LightningJit /// Function table used to store pointers to the functions that the guest code will call /// Cache used on platforms that enforce W^X, otherwise should be null /// is null - public TranslatorStubs(AddressTable functionTable, NoWxCache noWxCache) + public TranslatorStubs(IAddressTable functionTable, NoWxCache noWxCache) { ArgumentNullException.ThrowIfNull(functionTable); diff --git a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj index 5a6bf5c3d..0a55a7dea 100644 --- a/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj +++ b/src/Ryujinx.Cpu/Ryujinx.Cpu.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj index 973a9e260..58f54de7d 100644 --- a/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj +++ b/src/Ryujinx.Graphics.Device/Ryujinx.Graphics.Device.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.GAL/IRenderer.cs b/src/Ryujinx.Graphics.GAL/IRenderer.cs index 9b5e2cc42..c2fdcbe4b 100644 --- a/src/Ryujinx.Graphics.GAL/IRenderer.cs +++ b/src/Ryujinx.Graphics.GAL/IRenderer.cs @@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL IPipeline Pipeline { get; } IWindow Window { get; } - + uint ProgramCount { get; } void BackgroundContextAction(Action action, bool alwaysBackground = false); diff --git a/src/Ryujinx.Graphics.GAL/IWindow.cs b/src/Ryujinx.Graphics.GAL/IWindow.cs index 83418e709..12686cb28 100644 --- a/src/Ryujinx.Graphics.GAL/IWindow.cs +++ b/src/Ryujinx.Graphics.GAL/IWindow.cs @@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.GAL void SetSize(int width, int height); - void ChangeVSyncMode(bool vsyncEnabled); + void ChangeVSyncMode(VSyncMode vSyncMode); void SetAntiAliasing(AntiAliasing antialiasing); void SetScalingFilter(ScalingFilter type); diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs index acda37ef3..102fdb1bb 100644 --- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs +++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedWindow.cs @@ -31,7 +31,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading _impl.Window.SetSize(width, height); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangeVSyncMode(VSyncMode vSyncMode) { } public void SetAntiAliasing(AntiAliasing effect) { } diff --git a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj index d88b641a3..a230296c1 100644 --- a/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj +++ b/src/Ryujinx.Graphics.GAL/Ryujinx.Graphics.GAL.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.GAL/VSyncMode.cs b/src/Ryujinx.Graphics.GAL/VSyncMode.cs new file mode 100644 index 000000000..c5794b8f7 --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/VSyncMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Graphics.GAL +{ + public enum VSyncMode + { + Switch, + Unbounded, + Custom + } +} diff --git a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj index 6f1cce6ac..8c740fadc 100644 --- a/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj +++ b/src/Ryujinx.Graphics.Gpu/Ryujinx.Graphics.Gpu.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj index d631d039f..92077e26a 100644 --- a/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj +++ b/src/Ryujinx.Graphics.Host1x/Ryujinx.Graphics.Host1x.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj index d1a6358c2..7659c4b25 100644 --- a/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj +++ b/src/Ryujinx.Graphics.Nvdec.FFmpeg/Ryujinx.Graphics.Nvdec.FFmpeg.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj b/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj index d1a6358c2..7659c4b25 100644 --- a/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj +++ b/src/Ryujinx.Graphics.Nvdec.Vp9/Ryujinx.Graphics.Nvdec.Vp9.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj index 6c00e9a7c..7a13b5d1b 100644 --- a/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj +++ b/src/Ryujinx.Graphics.Nvdec/Ryujinx.Graphics.Nvdec.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs index 2deee045c..6ead314fd 100644 --- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs +++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs @@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info) { ProgramCount++; - + return new Program(shaders, info.FragmentOutputMap); } diff --git a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj index f3071f486..931e70c03 100644 --- a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj +++ b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs index 285ab725e..1dc8a51f6 100644 --- a/src/Ryujinx.Graphics.OpenGL/Window.cs +++ b/src/Ryujinx.Graphics.OpenGL/Window.cs @@ -54,7 +54,7 @@ namespace Ryujinx.Graphics.OpenGL GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4); } - public void ChangeVSyncMode(bool vsyncEnabled) { } + public void ChangeVSyncMode(VSyncMode vSyncMode) { } public void SetSize(int width, int height) { diff --git a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj index 8ccf5348f..be32641eb 100644 --- a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj +++ b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj b/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj index 51721490e..48d10f1d5 100644 --- a/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj +++ b/src/Ryujinx.Graphics.Texture/Ryujinx.Graphics.Texture.csproj @@ -2,6 +2,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj index a6c4fb2bb..820e807e6 100644 --- a/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj +++ b/src/Ryujinx.Graphics.Vic/Ryujinx.Graphics.Vic.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj b/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj index abff58a53..d85effe32 100644 --- a/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj +++ b/src/Ryujinx.Graphics.Video/Ryujinx.Graphics.Video.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj index aae28733f..b138e309a 100644 --- a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj +++ b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs index c9aab4018..436914330 100644 --- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs +++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs @@ -182,6 +182,16 @@ namespace Ryujinx.Graphics.Vulkan return false; } } + + //Prevent the sum of descriptors from exceeding MaxPushDescriptors + int totalDescriptors = 0; + foreach (ResourceDescriptor desc in layout.Sets.First().Descriptors) + { + if (!reserved.Contains(desc.Binding)) + totalDescriptors += desc.Count; + } + if (totalDescriptors > gd.Capabilities.MaxPushDescriptors) + return false; return true; } diff --git a/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs b/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs index 6f27bb68b..ce1293589 100644 --- a/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs +++ b/src/Ryujinx.Graphics.Vulkan/VertexBufferState.cs @@ -55,8 +55,10 @@ namespace Ryujinx.Graphics.Vulkan if (_handle != BufferHandle.Null) { // May need to restride the vertex buffer. - - if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && (_stride % alignment) != 0) + // + // Fix divide by zero when recovering from missed draw (Oct. 16 2024) + // (fixes crash in 'Baldo: The Guardian Owls' opening cutscene) + if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && alignment != 0 && (_stride % alignment) != 0) { autoBuffer = gd.BufferManager.GetAlignedVertexBuffer(cbs, _handle, _offset, _size, _stride, alignment); diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index 8d324957a..cc2bc36c2 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -549,7 +549,7 @@ namespace Ryujinx.Graphics.Vulkan public IProgram CreateProgram(ShaderSource[] sources, ShaderInfo info) { ProgramCount++; - + bool isCompute = sources.Length == 1 && sources[0].Stage == ShaderStage.Compute; if (info.State.HasValue || isCompute) diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs index 3dc6d4e19..3e8d3b375 100644 --- a/src/Ryujinx.Graphics.Vulkan/Window.cs +++ b/src/Ryujinx.Graphics.Vulkan/Window.cs @@ -29,7 +29,7 @@ namespace Ryujinx.Graphics.Vulkan private int _width; private int _height; - private bool _vsyncEnabled; + private VSyncMode _vSyncMode; private bool _swapchainIsDirty; private VkFormat _format; private AntiAliasing _currentAntiAliasing; @@ -139,7 +139,7 @@ namespace Ryujinx.Graphics.Vulkan ImageArrayLayers = 1, PreTransform = capabilities.CurrentTransform, CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha), - PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled), + PresentMode = ChooseSwapPresentMode(presentModes, _vSyncMode), Clipped = true, }; @@ -279,9 +279,9 @@ namespace Ryujinx.Graphics.Vulkan } } - private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled) + private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, VSyncMode vSyncMode) { - if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) + if (vSyncMode == VSyncMode.Unbounded && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr)) { return PresentModeKHR.ImmediateKhr; } @@ -634,9 +634,10 @@ namespace Ryujinx.Graphics.Vulkan _swapchainIsDirty = true; } - public override void ChangeVSyncMode(bool vsyncEnabled) + public override void ChangeVSyncMode(VSyncMode vSyncMode) { - _vsyncEnabled = vsyncEnabled; + _vSyncMode = vSyncMode; + //present mode may change, so mark the swapchain for recreation _swapchainIsDirty = true; } diff --git a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs index edb9c688c..ca06ec0b8 100644 --- a/src/Ryujinx.Graphics.Vulkan/WindowBase.cs +++ b/src/Ryujinx.Graphics.Vulkan/WindowBase.cs @@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Vulkan public abstract void Dispose(); public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback); public abstract void SetSize(int width, int height); - public abstract void ChangeVSyncMode(bool vsyncEnabled); + public abstract void ChangeVSyncMode(VSyncMode vSyncMode); public abstract void SetAntiAliasing(AntiAliasing effect); public abstract void SetScalingFilter(ScalingFilter scalerType); public abstract void SetScalingFilterLevel(float scale); diff --git a/src/Ryujinx.HLE.Generators/IpcServiceGenerator.cs b/src/Ryujinx.HLE.Generators/IpcServiceGenerator.cs index 5dcd49af5..5cac4d13a 100644 --- a/src/Ryujinx.HLE.Generators/IpcServiceGenerator.cs +++ b/src/Ryujinx.HLE.Generators/IpcServiceGenerator.cs @@ -13,6 +13,7 @@ namespace Ryujinx.HLE.Generators var syntaxReceiver = (ServiceSyntaxReceiver)context.SyntaxReceiver; CodeGenerator generator = new CodeGenerator(); + generator.AppendLine("#nullable enable"); generator.AppendLine("using System;"); generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm"); generator.EnterScope($"partial class IUserInterface"); @@ -58,6 +59,7 @@ namespace Ryujinx.HLE.Generators generator.LeaveScope(); generator.LeaveScope(); + generator.AppendLine("#nullable disable"); context.AddSource($"IUserInterface.g.cs", generator.ToString()); } diff --git a/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj b/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj index eeab9c0e9..4791a3b27 100644 --- a/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj +++ b/src/Ryujinx.HLE.Generators/Ryujinx.HLE.Generators.csproj @@ -6,6 +6,7 @@ true Generated true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index fc8def9d2..51f6058fc 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -21,6 +21,7 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using Path = System.IO.Path; namespace Ryujinx.HLE.FileSystem @@ -474,6 +475,74 @@ namespace Ryujinx.HLE.FileSystem FinishInstallation(temporaryDirectory, registeredDirectory); } + public void InstallKeys(string keysSource, string installDirectory) + { + if (Directory.Exists(keysSource)) + { + foreach (var filePath in Directory.EnumerateFiles(keysSource, "*.keys")) + { + VerifyKeysFile(filePath); + File.Copy(filePath, Path.Combine(installDirectory, Path.GetFileName(filePath)), true); + } + + return; + } + + if (!File.Exists(keysSource)) + { + throw new FileNotFoundException("Keys file does not exist."); + } + + FileInfo info = new(keysSource); + + using FileStream file = File.OpenRead(keysSource); + + switch (info.Extension) + { + case ".zip": + using (ZipArchive archive = ZipFile.OpenRead(keysSource)) + { + InstallKeysFromZip(archive, installDirectory); + } + break; + case ".keys": + VerifyKeysFile(keysSource); + File.Copy(keysSource, Path.Combine(installDirectory, info.Name), true); + break; + default: + throw new InvalidFirmwarePackageException("Input file is not a valid key package"); + } + } + + private void InstallKeysFromZip(ZipArchive archive, string installDirectory) + { + string temporaryDirectory = Path.Combine(installDirectory, "temp"); + if (Directory.Exists(temporaryDirectory)) + { + Directory.Delete(temporaryDirectory, true); + } + Directory.CreateDirectory(temporaryDirectory); + foreach (var entry in archive.Entries) + { + if (Path.GetExtension(entry.FullName).Equals(".keys", StringComparison.OrdinalIgnoreCase)) + { + string extractDestination = Path.Combine(temporaryDirectory, entry.Name); + entry.ExtractToFile(extractDestination, overwrite: true); + try + { + VerifyKeysFile(extractDestination); + File.Move(extractDestination, Path.Combine(installDirectory, entry.Name), true); + } + catch (Exception) + { + Directory.Delete(temporaryDirectory, true); + throw; + } + } + } + Directory.Delete(temporaryDirectory, true); + } + private void FinishInstallation(string temporaryDirectory, string registeredDirectory) { if (Directory.Exists(registeredDirectory)) @@ -947,5 +1016,70 @@ namespace Ryujinx.HLE.FileSystem return null; } + + public void VerifyKeysFile(string filePath) + { + // Verify the keys file format refers to https://github.com/Thealexbarney/LibHac/blob/master/KEYS.md + string genericPattern = @"^[a-z0-9_]+ = [a-z0-9]+$"; + string titlePattern = @"^[a-z0-9]{32} = [a-z0-9]{32}$"; + + if (File.Exists(filePath)) + { + // Read all lines from the file + string fileName = Path.GetFileName(filePath); + string[] lines = File.ReadAllLines(filePath); + + bool verified = false; + switch (fileName) + { + case "prod.keys": + verified = verifyKeys(lines, genericPattern); + break; + case "title.keys": + verified = verifyKeys(lines, titlePattern); + break; + case "console.keys": + verified = verifyKeys(lines, genericPattern); + break; + case "dev.keys": + verified = verifyKeys(lines, genericPattern); + break; + default: + throw new FormatException($"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported."); + } + if (!verified) + { + throw new FormatException($"Invalid \"{filePath}\" file format."); + } + } else + { + throw new FileNotFoundException($"Keys file not found at \"{filePath}\"."); + } + } + + private bool verifyKeys(string[] lines, string regex) + { + foreach (string line in lines) + { + if (!Regex.IsMatch(line, regex)) + { + return false; + } + } + return true; + } + + public bool AreKeysAlredyPresent(string pathToCheck) + { + string[] fileNames = { "prod.keys", "title.keys", "console.keys", "dev.keys" }; + foreach (var file in fileNames) + { + if (File.Exists(Path.Combine(pathToCheck, file))) + { + return true; + } + } + return false; + } } } diff --git a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs index 39c544eac..ef9c493a8 100644 --- a/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs +++ b/src/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs @@ -223,9 +223,10 @@ namespace Ryujinx.HLE.FileSystem { KeySet ??= KeySet.CreateDefaultKeySet(); - string keyFile = null; + string prodKeyFile = null; string titleKeyFile = null; string consoleKeyFile = null; + string devKeyFile = null; if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile) { @@ -236,13 +237,14 @@ namespace Ryujinx.HLE.FileSystem void LoadSetAtPath(string basePath) { - string localKeyFile = Path.Combine(basePath, "prod.keys"); + string localProdKeyFile = Path.Combine(basePath, "prod.keys"); string localTitleKeyFile = Path.Combine(basePath, "title.keys"); string localConsoleKeyFile = Path.Combine(basePath, "console.keys"); + string localDevKeyFile = Path.Combine(basePath, "dev.keys"); - if (File.Exists(localKeyFile)) + if (File.Exists(localProdKeyFile)) { - keyFile = localKeyFile; + prodKeyFile = localProdKeyFile; } if (File.Exists(localTitleKeyFile)) @@ -254,9 +256,14 @@ namespace Ryujinx.HLE.FileSystem { consoleKeyFile = localConsoleKeyFile; } + + if (File.Exists(localDevKeyFile)) + { + devKeyFile = localDevKeyFile; + } } - ExternalKeyReader.ReadKeyFile(KeySet, keyFile, titleKeyFile, consoleKeyFile, null); + ExternalKeyReader.ReadKeyFile(KeySet, prodKeyFile, devKeyFile, titleKeyFile, consoleKeyFile, null); } public void ImportTickets(IFileSystem fs) diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index 955fee4b5..f75ead588 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -9,6 +9,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.UI; using System; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.HLE { @@ -84,9 +85,14 @@ namespace Ryujinx.HLE internal readonly RegionCode Region; /// - /// Control the initial state of the vertical sync in the SurfaceFlinger service. + /// Control the initial state of the present interval in the SurfaceFlinger service (previously Vsync). /// - internal readonly bool EnableVsync; + internal readonly VSyncMode VSyncMode; + + /// + /// Control the custom VSync interval, if enabled and active. + /// + internal readonly int CustomVSyncInterval; /// /// Control the initial state of the docked mode. @@ -164,6 +170,21 @@ namespace Ryujinx.HLE /// public MultiplayerMode MultiplayerMode { internal get; set; } + /// + /// Disable P2P mode + /// + public bool MultiplayerDisableP2p { internal get; set; } + + /// + /// Multiplayer Passphrase + /// + public string MultiplayerLdnPassphrase { internal get; set; } + + /// + /// LDN Server + /// + public string MultiplayerLdnServer { internal get; set; } + /// /// An action called when HLE force a refresh of output after docked mode changed. /// @@ -180,7 +201,7 @@ namespace Ryujinx.HLE IHostUIHandler hostUIHandler, SystemLanguage systemLanguage, RegionCode region, - bool enableVsync, + VSyncMode vSyncMode, bool enableDockedMode, bool enablePtc, bool enableInternetAccess, @@ -194,7 +215,11 @@ namespace Ryujinx.HLE float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + bool multiplayerDisableP2p, + string multiplayerLdnPassphrase, + string multiplayerLdnServer, + int customVSyncInterval) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -207,7 +232,8 @@ namespace Ryujinx.HLE HostUIHandler = hostUIHandler; SystemLanguage = systemLanguage; Region = region; - EnableVsync = enableVsync; + VSyncMode = vSyncMode; + CustomVSyncInterval = customVSyncInterval; EnableDockedMode = enableDockedMode; EnablePtc = enablePtc; EnableInternetAccess = enableInternetAccess; @@ -222,6 +248,9 @@ namespace Ryujinx.HLE UseHypervisor = useHypervisor; MultiplayerLanInterfaceId = multiplayerLanInterfaceId; MultiplayerMode = multiplayerMode; + MultiplayerDisableP2p = multiplayerDisableP2p; + MultiplayerLdnPassphrase = multiplayerLdnPassphrase; + MultiplayerLdnServer = multiplayerLdnServer; } } } diff --git a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs index 3c34d5c78..a2ddd573d 100644 --- a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs +++ b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs @@ -1,4 +1,7 @@ +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Applets.Browser; +using Ryujinx.HLE.HOS.Applets.Cabinet; +using Ryujinx.HLE.HOS.Applets.Dummy; using Ryujinx.HLE.HOS.Applets.Error; using Ryujinx.HLE.HOS.Services.Am.AppletAE; using System; @@ -26,9 +29,15 @@ namespace Ryujinx.HLE.HOS.Applets return new BrowserApplet(system); case AppletId.LibAppletOff: return new BrowserApplet(system); + case AppletId.MiiEdit: + Logger.Warning?.Print(LogClass.Application, $"Please use the MiiEdit inside File/Open Applet"); + return new DummyApplet(system); + case AppletId.Cabinet: + return new CabinetApplet(system); } - throw new NotImplementedException($"{applet} applet is not implemented."); + Logger.Warning?.Print(LogClass.Application, $"Applet {applet} not implemented!"); + return new DummyApplet(system); } } } diff --git a/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs new file mode 100644 index 000000000..f4f935d34 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs @@ -0,0 +1,195 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using Ryujinx.HLE.HOS.Services.Hid.HidServer; +using Ryujinx.HLE.HOS.Services.Hid; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.HLE.HOS.Applets.Cabinet +{ + internal unsafe class CabinetApplet : IApplet + { + private readonly Horizon _system; + private AppletSession _normalSession; + + public event EventHandler AppletStateChanged; + + public CabinetApplet(Horizon system) + { + _system = system; + } + + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + _normalSession = normalSession; + + byte[] launchParams = _normalSession.Pop(); + byte[] startParamBytes = _normalSession.Pop(); + + StartParamForAmiiboSettings startParam = IApplet.ReadStruct(startParamBytes); + + Logger.Stub?.PrintStub(LogClass.ServiceAm, $"CabinetApplet Start Type: {startParam.Type}"); + + switch (startParam.Type) + { + case 0: + StartNicknameAndOwnerSettings(ref startParam); + break; + case 1: + case 3: + StartFormatter(ref startParam); + break; + default: + Logger.Error?.Print(LogClass.ServiceAm, $"Unknown AmiiboSettings type: {startParam.Type}"); + break; + } + + // Prepare the response + ReturnValueForAmiiboSettings returnValue = new() + { + AmiiboSettingsReturnFlag = (byte)AmiiboSettingsReturnFlag.HasRegisterInfo, + DeviceHandle = new DeviceHandle + { + Handle = 0 // Dummy device handle + }, + RegisterInfo = startParam.RegisterInfo + }; + + // Push the response + _normalSession.Push(BuildResponse(returnValue)); + AppletStateChanged?.Invoke(this, null); + + _system.ReturnFocus(); + + return ResultCode.Success; + } + + public ResultCode GetResult() + { + _system.Device.System.NfpDevices.RemoveAt(0); + return ResultCode.Success; + } + + private void StartFormatter(ref StartParamForAmiiboSettings startParam) + { + // Initialize RegisterInfo + startParam.RegisterInfo = new RegisterInfo(); + } + + private void StartNicknameAndOwnerSettings(ref StartParamForAmiiboSettings startParam) + { + _system.Device.UIHandler.DisplayCabinetDialog(out string newName); + byte[] nameBytes = Encoding.UTF8.GetBytes(newName); + Array41 nickName = new Array41(); + nameBytes.CopyTo(nickName.AsSpan()); + startParam.RegisterInfo.Nickname = nickName; + NfpDevice devicePlayer1 = new() + { + NpadIdType = NpadIdType.Player1, + Handle = HidUtils.GetIndexFromNpadIdType(NpadIdType.Player1), + State = NfpDeviceState.SearchingForTag, + }; + _system.Device.System.NfpDevices.Add(devicePlayer1); + _system.Device.UIHandler.DisplayCabinetMessageDialog(); + string amiiboId = string.Empty; + bool scanned = false; + while (!scanned) + { + for (int i = 0; i < _system.Device.System.NfpDevices.Count; i++) + { + if (_system.Device.System.NfpDevices[i].State == NfpDeviceState.TagFound) + { + amiiboId = _system.Device.System.NfpDevices[i].AmiiboId; + scanned = true; + } + } + } + VirtualAmiibo.UpdateNickName(amiiboId, newName); + } + + private static byte[] BuildResponse(ReturnValueForAmiiboSettings returnValue) + { + int size = Unsafe.SizeOf(); + byte[] bytes = new byte[size]; + + fixed (byte* bytesPtr = bytes) + { + Unsafe.Write(bytesPtr, returnValue); + } + + return bytes; + } + + public static T ReadStruct(byte[] data) where T : unmanaged + { + if (data.Length < Unsafe.SizeOf()) + { + throw new ArgumentException("Not enough data to read the struct"); + } + + fixed (byte* dataPtr = data) + { + return Unsafe.Read(dataPtr); + } + } + + #region Structs + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct TagInfo + { + public fixed byte Data[0x58]; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct StartParamForAmiiboSettings + { + public byte ZeroValue; // Left at zero by sdknso + public byte Type; + public byte Flags; + public byte AmiiboSettingsStartParamOffset28; + public ulong AmiiboSettingsStartParam0; + + public TagInfo TagInfo; // Only enabled when flags bit 1 is set + public RegisterInfo RegisterInfo; // Only enabled when flags bit 2 is set + + public fixed byte StartParamExtraData[0x20]; + + public fixed byte Reserved[0x24]; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct ReturnValueForAmiiboSettings + { + public byte AmiiboSettingsReturnFlag; + private byte Padding1; + private byte Padding2; + private byte Padding3; + public DeviceHandle DeviceHandle; + public TagInfo TagInfo; + public RegisterInfo RegisterInfo; + public fixed byte IgnoredBySdknso[0x24]; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct DeviceHandle + { + public ulong Handle; + } + + public enum AmiiboSettingsReturnFlag : byte + { + Cancel = 0, + HasTagInfo = 2, + HasRegisterInfo = 4, + HasTagInfoAndRegisterInfo = 6 + } + + #endregion + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs new file mode 100644 index 000000000..75df7a373 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs @@ -0,0 +1,43 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using System; +using System.IO; +using System.Runtime.InteropServices; +namespace Ryujinx.HLE.HOS.Applets.Dummy +{ + internal class DummyApplet : IApplet + { + private readonly Horizon _system; + private AppletSession _normalSession; + public event EventHandler AppletStateChanged; + public DummyApplet(Horizon system) + { + _system = system; + } + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + _normalSession = normalSession; + _normalSession.Push(BuildResponse()); + AppletStateChanged?.Invoke(this, null); + _system.ReturnFocus(); + return ResultCode.Success; + } + private static T ReadStruct(byte[] data) where T : struct + { + return MemoryMarshal.Read(data.AsSpan()); + } + private static byte[] BuildResponse() + { + using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); + using BinaryWriter writer = new(stream); + writer.Write((ulong)ResultCode.Success); + return stream.ToArray(); + } + public ResultCode GetResult() + { + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/ArmProcessContext.cs b/src/Ryujinx.HLE/HOS/ArmProcessContext.cs index fde489ab7..09a721644 100644 --- a/src/Ryujinx.HLE/HOS/ArmProcessContext.cs +++ b/src/Ryujinx.HLE/HOS/ArmProcessContext.cs @@ -13,7 +13,8 @@ namespace Ryujinx.HLE.HOS string displayVersion, bool diskCacheEnabled, ulong codeAddress, - ulong codeSize); + ulong codeSize, + string cacheSelector); } class ArmProcessContext : IArmProcessContext where T : class, IVirtualMemoryManagerTracked, IMemoryManager @@ -67,10 +68,11 @@ namespace Ryujinx.HLE.HOS string displayVersion, bool diskCacheEnabled, ulong codeAddress, - ulong codeSize) + ulong codeSize, + string cacheSelector) { _cpuContext.PrepareCodeRange(codeAddress, codeSize); - return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled); + return _cpuContext.LoadDiskCache(titleIdText, displayVersion, diskCacheEnabled, cacheSelector); } public void InvalidateCacheRegion(ulong address, ulong size) diff --git a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs index 6646826cb..14775fb1d 100644 --- a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs +++ b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs @@ -114,7 +114,7 @@ namespace Ryujinx.HLE.HOS } } - DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize); + DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, "default"); //Ready for exefs profiles return processContext; } diff --git a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs index 171a083f3..2e7b8ee76 100644 --- a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs +++ b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs @@ -2463,7 +2463,7 @@ namespace Ryujinx.HLE.HOS.Diagnostics.Demangler return ParseIntegerLiteral("unsigned short"); case 'i': _position++; - return ParseIntegerLiteral(""); + return ParseIntegerLiteral(string.Empty); case 'j': _position++; return ParseIntegerLiteral("u"); diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index ee179c929..7cbe1afca 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -116,18 +116,13 @@ namespace Ryujinx.HLE.HOS private readonly Dictionary _appMods; // key is ApplicationId private PatchCache _patches; - private static readonly EnumerationOptions _dirEnumOptions; - - static ModLoader() + private static readonly EnumerationOptions _dirEnumOptions = new() { - _dirEnumOptions = new EnumerationOptions - { - MatchCasing = MatchCasing.CaseInsensitive, - MatchType = MatchType.Simple, - RecurseSubdirectories = false, - ReturnSpecialDirectories = false, - }; - } + MatchCasing = MatchCasing.CaseInsensitive, + MatchType = MatchType.Simple, + RecurseSubdirectories = false, + ReturnSpecialDirectories = false, + }; public ModLoader() { @@ -169,7 +164,7 @@ namespace Ryujinx.HLE.HOS foreach (var modDir in dir.EnumerateDirectories()) { types.Clear(); - Mod mod = new("", null, true); + Mod mod = new(string.Empty, null, true); if (StrEquals(RomfsDir, modDir.Name)) { diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IAllSystemAppletProxiesService.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IAllSystemAppletProxiesService.cs index 0a032562a..b8741b22b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IAllSystemAppletProxiesService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/IAllSystemAppletProxiesService.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService; +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService; namespace Ryujinx.HLE.HOS.Services.Am.AppletAE { @@ -25,5 +26,14 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE return ResultCode.Success; } + + [CommandCmif(350)] + // OpenSystemApplicationProxy(u64, pid, handle) -> object + public ResultCode OpenSystemApplicationProxy(ServiceCtx context) + { + MakeObject(context, new IApplicationProxy(context.Request.HandleDesc.PId)); + + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/IServiceCreator.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/IServiceCreator.cs index 797a7a9bd..705e5f258 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/IServiceCreator.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/IServiceCreator.cs @@ -5,5 +5,23 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Lp2p class IServiceCreator : IpcService { public IServiceCreator(ServiceCtx context) { } + + [CommandCmif(0)] + // CreateNetworkService(pid, u64, u32) -> object + public ResultCode CreateNetworkService(ServiceCtx context) + { + MakeObject(context, new ISfService(context)); + + return ResultCode.Success; + } + + [CommandCmif(8)] + // CreateNetworkServiceMonitor(pid, u64) -> object + public ResultCode CreateNetworkServiceMonitor(ServiceCtx context) + { + MakeObject(context, new ISfServiceMonitor(context)); + + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfService.cs new file mode 100644 index 000000000..d48a88978 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfService.cs @@ -0,0 +1,45 @@ +using Ryujinx.Common.Logging; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Lp2p +{ + class ISfService : IpcService + { + public ISfService(ServiceCtx context) { } + + [CommandCmif(0)] + // Initialize() + public ResultCode Initialize(ServiceCtx context) + { + context.ResponseData.Write(0); + + return ResultCode.Success; + } + + [CommandCmif(768)] + // CreateGroup(buffer) + public ResultCode SendToOtherGroup(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceLdn); + + return ResultCode.Success; + } + + [CommandCmif(1544)] + // RecvFromOtherGroup(u32, buffer) -> (nn::lp2p::MacAddress, u16, s16, u32, s32) + public ResultCode RecvFromOtherGroup(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceLdn); + + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfServiceMonitor.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfServiceMonitor.cs new file mode 100644 index 000000000..d3a8bead2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfServiceMonitor.cs @@ -0,0 +1,86 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Horizon.Common; +using System; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Lp2p +{ + class ISfServiceMonitor : IpcService + { + private readonly KEvent _stateChangeEvent; + private readonly KEvent _jointEvent; + private int _stateChangeEventHandle = 0; + private int _jointEventHandle = 0; + + public ISfServiceMonitor(ServiceCtx context) + { + _stateChangeEvent = new KEvent(context.Device.System.KernelContext); + _jointEvent = new KEvent(context.Device.System.KernelContext); + } + + [CommandCmif(0)] + // Initialize() + public ResultCode Initialize(ServiceCtx context) + { + context.ResponseData.Write(0); + + return ResultCode.Success; + } + + [CommandCmif(256)] + // AttachNetworkInterfaceStateChangeEvent() -> handle + public ResultCode AttachNetworkInterfaceStateChangeEvent(ServiceCtx context) + { + if (context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out _stateChangeEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_stateChangeEventHandle); + + return ResultCode.Success; + } + + [CommandCmif(288)] + // GetGroupInfo(buffer) + public ResultCode GetGroupInfo(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceLdn); + + return ResultCode.Success; + } + + [CommandCmif(296)] + // GetGroupInfo2(buffer, buffer) + public ResultCode GetGroupInfo2(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceLdn); + + return ResultCode.Success; + } + + [CommandCmif(312)] + // GetIpConfig(buffer, 0x1a>) + public ResultCode GetIpConfig(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceLdn); + + return ResultCode.Success; + } + + [CommandCmif(328)] + // AttachNetworkInterfaceStateChangeEvent() -> handle + public ResultCode AttachJoinEvent(ServiceCtx context) + { + if (context.Process.HandleTable.GenerateHandle(_jointEvent.ReadableEvent, out _jointEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_jointEventHandle); + + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs index 4da5fe42b..c6d6ac944 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x20)] + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 8)] struct NetworkConfig { public IntentId IntentId; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs index 449c923cc..f3ab1edd5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x60)] + [StructLayout(LayoutKind.Sequential, Size = 0x60, Pack = 8)] struct ScanFilter { public NetworkId NetworkId; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs index 5939a1394..f3968aab4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x44)] + [StructLayout(LayoutKind.Sequential, Size = 0x44, Pack = 2)] struct SecurityConfig { public SecurityMode SecurityMode; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs index dbcaa9eeb..e564a2ec9 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x20)] + [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)] struct SecurityParameter { public Array16 Data; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs index 3820f936e..7246f6f80 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.Types { - [StructLayout(LayoutKind.Sequential, Size = 0x30)] + [StructLayout(LayoutKind.Sequential, Size = 0x30, Pack = 1)] struct UserConfig { public Array33 UserName; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs index 78ebcac82..bd00a3139 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs @@ -15,6 +15,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public Array8 LatestUpdates = new(); public bool Connected { get; private set; } + public ProxyConfig Config => _parent.NetworkClient.Config; + public AccessPoint(IUserLocalCommunicationService parent) { _parent = parent; @@ -24,9 +26,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public void Dispose() { - _parent.NetworkClient.DisconnectNetwork(); + if (_parent?.NetworkClient != null) + { + _parent.NetworkClient.DisconnectNetwork(); - _parent.NetworkClient.NetworkChange -= NetworkChanged; + _parent.NetworkClient.NetworkChange -= NetworkChanged; + } } private void NetworkChanged(object sender, NetworkChangeEventArgs e) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs index 7ad6de51d..028ab6cfc 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs @@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { interface INetworkClient : IDisposable { + ProxyConfig Config { get; } bool NeedsRealId { get; } event EventHandler NetworkChange; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 1d4b5485e..9f65aed4b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -9,6 +9,8 @@ using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; @@ -21,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class IUserLocalCommunicationService : IpcService, IDisposable { + public static string DefaultLanPlayHost = "ryuldn.vudjun.com"; + public static short LanPlayPort = 30456; + public INetworkClient NetworkClient { get; private set; } private const int NifmRequestID = 90; @@ -175,19 +180,37 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected) { - (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId); - - if (unicastAddress == null) + ProxyConfig config = _state switch { - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress)); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask)); + NetworkState.AccessPointCreated => _accessPoint.Config, + NetworkState.StationConnected => _station.Config, + + _ => default + }; + + if (config.ProxyIp == 0) + { + (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId); + + if (unicastAddress == null) + { + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress)); + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask)); + } + else + { + Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); + context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); + } } else { - Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\"."); + Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP."); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address)); - context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask)); + context.ResponseData.Write(config.ProxyIp); + context.ResponseData.Write(config.ProxySubnetMask); } } else @@ -1066,6 +1089,27 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator switch (mode) { + case MultiplayerMode.LdnRyu: + try + { + string ldnServer = context.Device.Configuration.MultiplayerLdnServer; + if (string.IsNullOrEmpty(ldnServer)) + { + ldnServer = DefaultLanPlayHost; + } + if (!IPAddress.TryParse(ldnServer, out IPAddress ipAddress)) + { + ipAddress = Dns.GetHostEntry(ldnServer).AddressList[0]; + } + NetworkClient = new LdnMasterProxyClient(ipAddress.ToString(), LanPlayPort, context.Device.Configuration); + } + catch (Exception ex) + { + Logger.Error?.Print(LogClass.ServiceLdn, "Could not locate LdnRyu server. Defaulting to stubbed wireless."); + Logger.Error?.Print(LogClass.ServiceLdn, ex.Message); + NetworkClient = new LdnDisabledClient(); + } + break; case MultiplayerMode.LdnMitm: NetworkClient = new LdnMitmClient(context.Device.Configuration); break; @@ -1103,7 +1147,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator _accessPoint?.Dispose(); _accessPoint = null; - NetworkClient?.Dispose(); + NetworkClient?.DisconnectAndStop(); NetworkClient = null; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs index e3385a1ed..2e8bb8d83 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Ldn.Types; using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; using System; @@ -6,12 +7,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class LdnDisabledClient : INetworkClient { + public ProxyConfig Config { get; } public bool NeedsRealId => true; public event EventHandler NetworkChange; public NetworkError Connect(ConnectRequest request) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return NetworkError.None; @@ -19,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public NetworkError ConnectPrivate(ConnectPrivateRequest request) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return NetworkError.None; @@ -26,6 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return true; @@ -33,6 +38,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!"); NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false)); return true; @@ -49,6 +55,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) { + Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to scan for networks, but Multiplayer is disabled!"); return Array.Empty(); } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs index 273acdd5e..40697d122 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs @@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm /// internal class LdnMitmClient : INetworkClient { + public ProxyConfig Config { get; } public bool NeedsRealId => false; public event EventHandler NetworkChange; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs new file mode 100644 index 000000000..a7c435506 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs @@ -0,0 +1,7 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + interface IProxyClient + { + bool SendAsync(byte[] buffer); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs new file mode 100644 index 000000000..4c7814b8e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs @@ -0,0 +1,645 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using TcpClient = NetCoreServer.TcpClient; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class LdnMasterProxyClient : TcpClient, INetworkClient, IProxyClient + { + public bool NeedsRealId => true; + + private static InitializeMessage InitializeMemory = new InitializeMessage(); + + private const int InactiveTimeout = 6000; + private const int FailureTimeout = 4000; + private const int ScanTimeout = 1000; + + private bool _useP2pProxy; + private NetworkError _lastError; + + private readonly ManualResetEvent _connected = new ManualResetEvent(false); + private readonly ManualResetEvent _error = new ManualResetEvent(false); + private readonly ManualResetEvent _scan = new ManualResetEvent(false); + private readonly ManualResetEvent _reject = new ManualResetEvent(false); + private readonly AutoResetEvent _apConnected = new AutoResetEvent(false); + + private readonly RyuLdnProtocol _protocol; + private readonly NetworkTimeout _timeout; + + private readonly List _availableGames = new List(); + private DisconnectReason _disconnectReason; + + private P2pProxyServer _hostedProxy; + private P2pProxyClient _connectedProxy; + + private bool _networkConnected; + + private string _passphrase; + private byte[] _gameVersion = new byte[0x10]; + + private readonly HLEConfiguration _config; + + public event EventHandler NetworkChange; + + public ProxyConfig Config { get; private set; } + + public LdnMasterProxyClient(string address, int port, HLEConfiguration config) : base(address, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + _protocol = new RyuLdnProtocol(); + _timeout = new NetworkTimeout(InactiveTimeout, TimeoutConnection); + + _protocol.Initialize += HandleInitialize; + _protocol.Connected += HandleConnected; + _protocol.Reject += HandleReject; + _protocol.RejectReply += HandleRejectReply; + _protocol.SyncNetwork += HandleSyncNetwork; + _protocol.ProxyConfig += HandleProxyConfig; + _protocol.Disconnected += HandleDisconnected; + + _protocol.ScanReply += HandleScanReply; + _protocol.ScanReplyEnd += HandleScanReplyEnd; + _protocol.ExternalProxy += HandleExternalProxy; + + _protocol.Ping += HandlePing; + _protocol.NetworkError += HandleNetworkError; + + _config = config; + _useP2pProxy = !config.MultiplayerDisableP2p; + } + + private void TimeoutConnection() + { + _connected.Reset(); + + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + } + + private bool EnsureConnected() + { + if (IsConnected) + { + return true; + } + + _error.Reset(); + + ConnectAsync(); + + int index = WaitHandle.WaitAny(new WaitHandle[] { _connected, _error }, FailureTimeout); + + if (IsConnected) + { + SendAsync(_protocol.Encode(PacketId.Initialize, InitializeMemory)); + } + + return index == 0 && IsConnected; + } + + private void UpdatePassphraseIfNeeded() + { + string passphrase = _config.MultiplayerLdnPassphrase ?? ""; + if (passphrase != _passphrase) + { + _passphrase = passphrase; + + SendAsync(_protocol.Encode(PacketId.Passphrase, StringUtils.GetFixedLengthBytes(passphrase, 0x80, Encoding.UTF8))); + } + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}"); + + UpdatePassphraseIfNeeded(); + + _connected.Set(); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}"); + + _passphrase = null; + + _connected.Reset(); + + if (_networkConnected) + { + DisconnectInternal(); + } + } + + public void DisconnectAndStop() + { + _timeout.Dispose(); + + DisconnectAsync(); + + while (IsConnected) + { + Thread.Yield(); + } + + Dispose(); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(buffer, (int)offset, (int)size); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}"); + + _error.Set(); + } + + + + private void HandleInitialize(LdnHeader header, InitializeMessage initialize) + { + InitializeMemory = initialize; + } + + private void HandleExternalProxy(LdnHeader header, ExternalProxyConfig config) + { + int length = config.AddressFamily switch + { + AddressFamily.InterNetwork => 4, + AddressFamily.InterNetworkV6 => 16, + _ => 0 + }; + + if (length == 0) + { + return; // Invalid external proxy. + } + + IPAddress address = new(config.ProxyIp.AsSpan()[..length].ToArray()); + P2pProxyClient proxy = new(address.ToString(), config.ProxyPort); + + _connectedProxy = proxy; + + bool success = proxy.PerformAuth(config); + + if (!success) + { + DisconnectInternal(); + } + } + + private void HandlePing(LdnHeader header, PingMessage ping) + { + if (ping.Requester == 0) // Server requested. + { + // Send the ping message back. + + SendAsync(_protocol.Encode(PacketId.Ping, ping)); + } + } + + private void HandleNetworkError(LdnHeader header, NetworkErrorMessage error) + { + if (error.Error == NetworkError.PortUnreachable) + { + _useP2pProxy = false; + } + else + { + _lastError = error.Error; + } + } + + private NetworkError ConsumeNetworkError() + { + NetworkError result = _lastError; + + _lastError = NetworkError.None; + + return result; + } + + private void HandleSyncNetwork(LdnHeader header, NetworkInfo info) + { + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true)); + } + + private void HandleConnected(LdnHeader header, NetworkInfo info) + { + _networkConnected = true; + _disconnectReason = DisconnectReason.None; + + _apConnected.Set(); + + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true)); + } + + private void HandleDisconnected(LdnHeader header, DisconnectMessage message) + { + DisconnectInternal(); + } + + private void HandleReject(LdnHeader header, RejectRequest reject) + { + // When the client receives a Reject request, we have been rejected and will be disconnected shortly. + _disconnectReason = reject.DisconnectReason; + } + + private void HandleRejectReply(LdnHeader header) + { + _reject.Set(); + } + + private void HandleScanReply(LdnHeader header, NetworkInfo info) + { + _availableGames.Add(info); + } + + private void HandleScanReplyEnd(LdnHeader obj) + { + _scan.Set(); + } + + private void DisconnectInternal() + { + if (_networkConnected) + { + _networkConnected = false; + + _hostedProxy?.Dispose(); + _hostedProxy = null; + + _connectedProxy?.Dispose(); + _connectedProxy = null; + + _apConnected.Reset(); + + NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false, _disconnectReason)); + + if (IsConnected) + { + _timeout.RefreshTimeout(); + } + } + } + + public void DisconnectNetwork() + { + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.Disconnect, new DisconnectMessage())); + + DisconnectInternal(); + } + } + + public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId) + { + if (_networkConnected) + { + _reject.Reset(); + + SendAsync(_protocol.Encode(PacketId.Reject, new RejectRequest(disconnectReason, nodeId))); + + int index = WaitHandle.WaitAny(new WaitHandle[] { _reject, _error }, InactiveTimeout); + + if (index == 0) + { + return (ConsumeNetworkError() != NetworkError.None) ? ResultCode.InvalidState : ResultCode.Success; + } + } + + return ResultCode.InvalidState; + } + + public void SetAdvertiseData(byte[] data) + { + // TODO: validate we're the owner (the server will do this anyways tho) + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.SetAdvertiseData, data)); + } + } + + public void SetGameVersion(byte[] versionString) + { + _gameVersion = versionString; + + if (_gameVersion.Length < 0x10) + { + Array.Resize(ref _gameVersion, 0x10); + } + } + + public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy) + { + // TODO: validate we're the owner (the server will do this anyways tho) + if (_networkConnected) + { + SendAsync(_protocol.Encode(PacketId.SetAcceptPolicy, new SetAcceptPolicyRequest + { + StationAcceptPolicy = acceptPolicy + })); + } + } + + private void DisposeProxy() + { + _hostedProxy?.Dispose(); + _hostedProxy = null; + } + + private void ConfigureAccessPoint(ref RyuNetworkConfig request) + { + _gameVersion.AsSpan().CopyTo(request.GameVersion.AsSpan()); + + if (_useP2pProxy) + { + // Before sending the request, attempt to set up a proxy server. + // This can be on a range of private ports, which can be exposed on a range of public + // ports via UPnP. If any of this fails, we just fall back to using the master server. + + int i = 0; + for (; i < P2pProxyServer.PrivatePortRange; i++) + { + _hostedProxy = new P2pProxyServer(this, (ushort)(P2pProxyServer.PrivatePortBase + i), _protocol); + + try + { + _hostedProxy.Start(); + + break; + } + catch (SocketException e) + { + _hostedProxy.Dispose(); + _hostedProxy = null; + + if (e.SocketErrorCode != SocketError.AddressAlreadyInUse) + { + i = P2pProxyServer.PrivatePortRange; // Immediately fail. + } + } + } + + bool openSuccess = i < P2pProxyServer.PrivatePortRange; + + if (openSuccess) + { + Task natPunchResult = _hostedProxy.NatPunch(); + + try + { + if (natPunchResult.Result != 0) + { + // Tell the server that we are hosting the proxy. + request.ExternalProxyPort = natPunchResult.Result; + } + } + catch (Exception) { } + + if (request.ExternalProxyPort == 0) + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Failed to open a port with UPnP for P2P connection. Proxying through the master server instead. Expect higher latency."); + _hostedProxy.Dispose(); + } + else + { + Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}."); + _hostedProxy.Start(); + + (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(); + + unicastAddress.Address.GetAddressBytes().AsSpan().CopyTo(request.PrivateIp.AsSpan()); + request.InternalProxyPort = _hostedProxy.PrivatePort; + request.AddressFamily = unicastAddress.Address.AddressFamily; + } + } + else + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Cannot create a P2P server. Proxying through the master server instead. Expect higher latency."); + } + } + } + + private bool CreateNetworkCommon() + { + bool signalled = _apConnected.WaitOne(FailureTimeout); + + if (!_useP2pProxy && _hostedProxy != null) + { + Logger.Warning?.Print(LogClass.ServiceLdn, "Locally hosted proxy server was not externally reachable. Proxying through the master server instead. Expect higher latency."); + + DisposeProxy(); + } + + if (signalled && _connectedProxy != null) + { + _connectedProxy.EnsureProxyReady(); + + Config = _connectedProxy.ProxyConfig; + } + else + { + DisposeProxy(); + } + + return signalled; + } + + public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData) + { + _timeout.DisableTimeout(); + + ConfigureAccessPoint(ref request.RyuNetworkConfig); + + if (!EnsureConnected()) + { + DisposeProxy(); + + return false; + } + + UpdatePassphraseIfNeeded(); + + SendAsync(_protocol.Encode(PacketId.CreateAccessPoint, request, advertiseData)); + + // Send a network change event with dummy data immediately. Necessary to avoid crashes in some games + var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo() + { + Common = new CommonNetworkInfo() + { + MacAddress = InitializeMemory.MacAddress, + Channel = request.NetworkConfig.Channel, + LinkLevel = 3, + NetworkType = 2, + Ssid = new Ssid() + { + Length = 32 + } + }, + Ldn = new LdnNetworkInfo() + { + AdvertiseDataSize = (ushort)advertiseData.Length, + AuthenticationId = 0, + NodeCount = 1, + NodeCountMax = request.NetworkConfig.NodeCountMax, + SecurityMode = (ushort)request.SecurityConfig.SecurityMode + } + }, true); + networkChangeEvent.Info.Ldn.Nodes[0] = new NodeInfo() + { + Ipv4Address = 175243265, + IsConnected = 1, + LocalCommunicationVersion = request.NetworkConfig.LocalCommunicationVersion, + MacAddress = InitializeMemory.MacAddress, + NodeId = 0, + UserName = request.UserConfig.UserName + }; + "12345678123456781234567812345678"u8.ToArray().CopyTo(networkChangeEvent.Info.Common.Ssid.Name.AsSpan()); + NetworkChange?.Invoke(this, networkChangeEvent); + + return CreateNetworkCommon(); + } + + public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData) + { + _timeout.DisableTimeout(); + + ConfigureAccessPoint(ref request.RyuNetworkConfig); + + if (!EnsureConnected()) + { + DisposeProxy(); + + return false; + } + + UpdatePassphraseIfNeeded(); + + SendAsync(_protocol.Encode(PacketId.CreateAccessPointPrivate, request, advertiseData)); + + return CreateNetworkCommon(); + } + + public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter) + { + if (!_networkConnected) + { + _timeout.RefreshTimeout(); + } + + _availableGames.Clear(); + + int index = -1; + + if (EnsureConnected()) + { + UpdatePassphraseIfNeeded(); + + _scan.Reset(); + + SendAsync(_protocol.Encode(PacketId.Scan, scanFilter)); + + index = WaitHandle.WaitAny(new WaitHandle[] { _scan, _error }, ScanTimeout); + } + + if (index != 0) + { + // An error occurred or timeout. Write 0 games. + return Array.Empty(); + } + + return _availableGames.ToArray(); + } + + private NetworkError ConnectCommon() + { + bool signalled = _apConnected.WaitOne(FailureTimeout); + + NetworkError error = ConsumeNetworkError(); + + if (error != NetworkError.None) + { + return error; + } + + if (signalled && _connectedProxy != null) + { + _connectedProxy.EnsureProxyReady(); + + Config = _connectedProxy.ProxyConfig; + } + + return signalled ? NetworkError.None : NetworkError.ConnectTimeout; + } + + public NetworkError Connect(ConnectRequest request) + { + _timeout.DisableTimeout(); + + if (!EnsureConnected()) + { + return NetworkError.Unknown; + } + + SendAsync(_protocol.Encode(PacketId.Connect, request)); + + var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo() + { + Common = request.NetworkInfo.Common, + Ldn = request.NetworkInfo.Ldn + }, true); + + NetworkChange?.Invoke(this, networkChangeEvent); + + return ConnectCommon(); + } + + public NetworkError ConnectPrivate(ConnectPrivateRequest request) + { + _timeout.DisableTimeout(); + + if (!EnsureConnected()) + { + return NetworkError.Unknown; + } + + SendAsync(_protocol.Encode(PacketId.ConnectPrivate, request)); + + return ConnectCommon(); + } + + private void HandleProxyConfig(LdnHeader header, ProxyConfig config) + { + Config = config; + + SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol)); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs new file mode 100644 index 000000000..5012d5d81 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class NetworkTimeout : IDisposable + { + private readonly int _idleTimeout; + private readonly Action _timeoutCallback; + private CancellationTokenSource _cancel; + + private readonly object _lock = new object(); + + public NetworkTimeout(int idleTimeout, Action timeoutCallback) + { + _idleTimeout = idleTimeout; + _timeoutCallback = timeoutCallback; + } + + private async Task TimeoutTask() + { + CancellationTokenSource cts; + + lock (_lock) + { + cts = _cancel; + } + + if (cts == null) + { + return; + } + + try + { + await Task.Delay(_idleTimeout, cts.Token); + } + catch (TaskCanceledException) + { + return; // Timeout cancelled. + } + + lock (_lock) + { + // Run the timeout callback. If the cancel token source has been replaced, we have _just_ been cancelled. + if (cts == _cancel) + { + _timeoutCallback(); + } + } + } + + public bool RefreshTimeout() + { + lock (_lock) + { + _cancel?.Cancel(); + + _cancel = new CancellationTokenSource(); + + Task.Run(TimeoutTask); + } + + return true; + } + + public void DisableTimeout() + { + lock (_lock) + { + _cancel?.Cancel(); + + _cancel = new CancellationTokenSource(); + } + } + + public void Dispose() + { + DisableTimeout(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs new file mode 100644 index 000000000..bc3a5edf2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + public class EphemeralPortPool + { + private const ushort EphemeralBase = 49152; + + private readonly List _ephemeralPorts = new List(); + + private readonly object _lock = new object(); + + public ushort Get() + { + ushort port = EphemeralBase; + lock (_lock) + { + // Starting at the ephemeral port base, return an ephemeral port that is not in use. + // Returns 0 if the range is exhausted. + + for (int i = 0; i < _ephemeralPorts.Count; i++) + { + ushort existingPort = _ephemeralPorts[i]; + + if (existingPort > port) + { + // The port was free - take it. + _ephemeralPorts.Insert(i, port); + + return port; + } + + port++; + } + + if (port != 0) + { + _ephemeralPorts.Add(port); + } + + return port; + } + } + + public void Return(ushort port) + { + lock (_lock) + { + _ephemeralPorts.Remove(port); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs new file mode 100644 index 000000000..bb390d49a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs @@ -0,0 +1,254 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class LdnProxy : IDisposable + { + public EndPoint LocalEndpoint { get; } + public IPAddress LocalAddress { get; } + + private readonly List _sockets = new List(); + private readonly Dictionary _ephemeralPorts = new Dictionary(); + + private readonly IProxyClient _parent; + private RyuLdnProtocol _protocol; + private readonly uint _subnetMask; + private readonly uint _localIp; + private readonly uint _broadcast; + + public LdnProxy(ProxyConfig config, IProxyClient client, RyuLdnProtocol protocol) + { + _parent = client; + _protocol = protocol; + + _ephemeralPorts[ProtocolType.Udp] = new EphemeralPortPool(); + _ephemeralPorts[ProtocolType.Tcp] = new EphemeralPortPool(); + + byte[] address = BitConverter.GetBytes(config.ProxyIp); + Array.Reverse(address); + LocalAddress = new IPAddress(address); + + _subnetMask = config.ProxySubnetMask; + _localIp = config.ProxyIp; + _broadcast = _localIp | (~_subnetMask); + + RegisterHandlers(protocol); + } + + public bool Supported(AddressFamily domain, SocketType type, ProtocolType protocol) + { + if (protocol == ProtocolType.Tcp) + { + Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Tcp proxy networking is untested. Please report this game so that it can be tested."); + } + return domain == AddressFamily.InterNetwork && (protocol == ProtocolType.Tcp || protocol == ProtocolType.Udp); + } + + private void RegisterHandlers(RyuLdnProtocol protocol) + { + protocol.ProxyConnect += HandleConnectionRequest; + protocol.ProxyConnectReply += HandleConnectionResponse; + protocol.ProxyData += HandleData; + protocol.ProxyDisconnect += HandleDisconnect; + + _protocol = protocol; + } + + public void UnregisterHandlers(RyuLdnProtocol protocol) + { + protocol.ProxyConnect -= HandleConnectionRequest; + protocol.ProxyConnectReply -= HandleConnectionResponse; + protocol.ProxyData -= HandleData; + protocol.ProxyDisconnect -= HandleDisconnect; + } + + public ushort GetEphemeralPort(ProtocolType type) + { + return _ephemeralPorts[type].Get(); + } + + public void ReturnEphemeralPort(ProtocolType type, ushort port) + { + _ephemeralPorts[type].Return(port); + } + + public void RegisterSocket(LdnProxySocket socket) + { + lock (_sockets) + { + _sockets.Add(socket); + } + } + + public void UnregisterSocket(LdnProxySocket socket) + { + lock (_sockets) + { + _sockets.Remove(socket); + } + } + + private void ForRoutedSockets(ProxyInfo info, Action action) + { + lock (_sockets) + { + foreach (LdnProxySocket socket in _sockets) + { + // Must match protocol and destination port. + if (socket.ProtocolType != info.Protocol || socket.LocalEndPoint is not IPEndPoint endpoint || endpoint.Port != info.DestPort) + { + continue; + } + + // We can assume packets routed to us have been sent to our destination. + // They will either be sent to us, or broadcast packets. + + action(socket); + } + } + } + + public void HandleConnectionRequest(LdnHeader header, ProxyConnectRequest request) + { + ForRoutedSockets(request.Info, (socket) => + { + socket.HandleConnectRequest(request); + }); + } + + public void HandleConnectionResponse(LdnHeader header, ProxyConnectResponse response) + { + ForRoutedSockets(response.Info, (socket) => + { + socket.HandleConnectResponse(response); + }); + } + + public void HandleData(LdnHeader header, ProxyDataHeader proxyHeader, byte[] data) + { + ProxyDataPacket packet = new ProxyDataPacket() { Header = proxyHeader, Data = data }; + + ForRoutedSockets(proxyHeader.Info, (socket) => + { + socket.IncomingData(packet); + }); + } + + public void HandleDisconnect(LdnHeader header, ProxyDisconnectMessage disconnect) + { + ForRoutedSockets(disconnect.Info, (socket) => + { + socket.HandleDisconnect(disconnect); + }); + } + + private uint GetIpV4(IPEndPoint endpoint) + { + if (endpoint.AddressFamily != AddressFamily.InterNetwork) + { + throw new NotSupportedException(); + } + + byte[] address = endpoint.Address.GetAddressBytes(); + Array.Reverse(address); + + return BitConverter.ToUInt32(address); + } + + private ProxyInfo MakeInfo(IPEndPoint localEp, IPEndPoint remoteEP, ProtocolType type) + { + return new ProxyInfo + { + SourceIpV4 = GetIpV4(localEp), + SourcePort = (ushort)localEp.Port, + + DestIpV4 = GetIpV4(remoteEP), + DestPort = (ushort)remoteEP.Port, + + Protocol = type + }; + } + + public void RequestConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must ask the other side to initialize a connection, so they can accept a socket for us. + + ProxyConnectRequest request = new ProxyConnectRequest + { + Info = MakeInfo(localEp, remoteEp, type) + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnect, request)); + } + + public void SignalConnected(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must tell the other side that we have accepted their request for connection. + + ProxyConnectResponse request = new ProxyConnectResponse + { + Info = MakeInfo(localEp, remoteEp, type) + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnectReply, request)); + } + + public void EndConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We must tell the other side that our connection is dropped. + + ProxyDisconnectMessage request = new ProxyDisconnectMessage + { + Info = MakeInfo(localEp, remoteEp, type), + DisconnectReason = 0 // TODO + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyDisconnect, request)); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type) + { + // We send exactly as much as the user wants us to, currently instantly. + // TODO: handle over "virtual mtu" (we have a max packet size to worry about anyways). fragment if tcp? throw if udp? + + ProxyDataHeader request = new ProxyDataHeader + { + Info = MakeInfo(localEp, remoteEp, type), + DataLength = (uint)buffer.Length + }; + + _parent.SendAsync(_protocol.Encode(PacketId.ProxyData, request, buffer.ToArray())); + + return buffer.Length; + } + + public bool IsBroadcast(uint ip) + { + return ip == _broadcast; + } + + public bool IsMyself(uint ip) + { + return ip == _localIp; + } + + public void Dispose() + { + UnregisterHandlers(_protocol); + + lock (_sockets) + { + foreach (LdnProxySocket socket in _sockets) + { + socket.ProxyDestroyed(); + } + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs new file mode 100644 index 000000000..ed7a9c751 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs @@ -0,0 +1,797 @@ +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + /// + /// This socket is forwarded through a TCP stream that goes through the Ldn server. + /// The Ldn server will then route the packets we send (or need to receive) within the virtual adhoc network. + /// + class LdnProxySocket : ISocketImpl + { + private readonly LdnProxy _proxy; + + private bool _isListening; + private readonly List _listenSockets = new List(); + + private readonly Queue _connectRequests = new Queue(); + + private readonly AutoResetEvent _acceptEvent = new AutoResetEvent(false); + private readonly int _acceptTimeout = -1; + + private readonly Queue _errors = new Queue(); + + private readonly AutoResetEvent _connectEvent = new AutoResetEvent(false); + private ProxyConnectResponse _connectResponse; + + private int _receiveTimeout = -1; + private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false); + private readonly Queue _receiveQueue = new Queue(); + + // private int _sendTimeout = -1; // Sends are techically instant right now, so not _really_ used. + + private bool _connecting; + private bool _broadcast; + private bool _readShutdown; + // private bool _writeShutdown; + private bool _closed; + + private readonly Dictionary _socketOptions = new Dictionary() + { + { SocketOptionName.Broadcast, 0 }, //TODO: honor this value + { SocketOptionName.DontLinger, 0 }, + { SocketOptionName.Debug, 0 }, + { SocketOptionName.Error, 0 }, + { SocketOptionName.KeepAlive, 0 }, + { SocketOptionName.OutOfBandInline, 0 }, + { SocketOptionName.ReceiveBuffer, 131072 }, + { SocketOptionName.ReceiveTimeout, -1 }, + { SocketOptionName.SendBuffer, 131072 }, + { SocketOptionName.SendTimeout, -1 }, + { SocketOptionName.Type, 0 }, + { SocketOptionName.ReuseAddress, 0 } //TODO: honor this value + }; + + public EndPoint RemoteEndPoint { get; private set; } + + public EndPoint LocalEndPoint { get; private set; } + + public bool Connected { get; private set; } + + public bool IsBound { get; private set; } + + public AddressFamily AddressFamily { get; } + + public SocketType SocketType { get; } + + public ProtocolType ProtocolType { get; } + + public bool Blocking { get; set; } + + public int Available + { + get + { + int result = 0; + + lock (_receiveQueue) + { + foreach (ProxyDataPacket data in _receiveQueue) + { + result += data.Data.Length; + } + } + + return result; + } + } + + public bool Readable + { + get + { + if (_isListening) + { + lock (_connectRequests) + { + return _connectRequests.Count > 0; + } + } + else + { + if (_readShutdown) + { + return true; + } + + lock (_receiveQueue) + { + return _receiveQueue.Count > 0; + } + } + + } + } + public bool Writable => Connected || ProtocolType == ProtocolType.Udp; + public bool Error => false; + + public LdnProxySocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, LdnProxy proxy) + { + AddressFamily = addressFamily; + SocketType = socketType; + ProtocolType = protocolType; + + _proxy = proxy; + _socketOptions[SocketOptionName.Type] = (int)socketType; + + proxy.RegisterSocket(this); + } + + private IPEndPoint EnsureLocalEndpoint(bool replace) + { + if (LocalEndPoint != null) + { + if (replace) + { + _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port); + } + else + { + return (IPEndPoint)LocalEndPoint; + } + } + + IPEndPoint localEp = new IPEndPoint(_proxy.LocalAddress, _proxy.GetEphemeralPort(ProtocolType)); + LocalEndPoint = localEp; + + return localEp; + } + + public LdnProxySocket AsAccepted(IPEndPoint remoteEp) + { + Connected = true; + RemoteEndPoint = remoteEp; + + IPEndPoint localEp = EnsureLocalEndpoint(true); + + _proxy.SignalConnected(localEp, remoteEp, ProtocolType); + + return this; + } + + private void SignalError(WsaError error) + { + lock (_errors) + { + _errors.Enqueue((int)error); + } + } + + private IPEndPoint GetEndpoint(uint ipv4, ushort port) + { + byte[] address = BitConverter.GetBytes(ipv4); + Array.Reverse(address); + + return new IPEndPoint(new IPAddress(address), port); + } + + public void IncomingData(ProxyDataPacket packet) + { + bool isBroadcast = _proxy.IsBroadcast(packet.Header.Info.DestIpV4); + + if (!_closed && (_broadcast || !isBroadcast)) + { + lock (_receiveQueue) + { + _receiveQueue.Enqueue(packet); + } + } + } + + public ISocketImpl Accept() + { + if (!_isListening) + { + throw new InvalidOperationException(); + } + + // Accept a pending request to this socket. + + lock (_connectRequests) + { + if (!Blocking && _connectRequests.Count == 0) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + while (true) + { + _acceptEvent.WaitOne(_acceptTimeout); + + lock (_connectRequests) + { + while (_connectRequests.Count > 0) + { + ProxyConnectRequest request = _connectRequests.Dequeue(); + + if (_connectRequests.Count > 0) + { + _acceptEvent.Set(); // Still more accepts to do. + } + + // Is this request made for us? + IPEndPoint endpoint = GetEndpoint(request.Info.DestIpV4, request.Info.DestPort); + + if (Equals(endpoint, LocalEndPoint)) + { + // Yes - let's accept. + IPEndPoint remoteEndpoint = GetEndpoint(request.Info.SourceIpV4, request.Info.SourcePort); + + LdnProxySocket socket = new LdnProxySocket(AddressFamily, SocketType, ProtocolType, _proxy).AsAccepted(remoteEndpoint); + + lock (_listenSockets) + { + _listenSockets.Add(socket); + } + + return socket; + } + } + } + } + } + + public void Bind(EndPoint localEP) + { + ArgumentNullException.ThrowIfNull(localEP); + + if (LocalEndPoint != null) + { + _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port); + } + var asIPEndpoint = (IPEndPoint)localEP; + if (asIPEndpoint.Port == 0) + { + asIPEndpoint.Port = (ushort)_proxy.GetEphemeralPort(ProtocolType); + } + + LocalEndPoint = (IPEndPoint)localEP; + + IsBound = true; + } + + public void Close() + { + _closed = true; + + _proxy.UnregisterSocket(this); + + if (Connected) + { + Disconnect(false); + } + + lock (_listenSockets) + { + foreach (LdnProxySocket socket in _listenSockets) + { + socket.Close(); + } + } + + _isListening = false; + } + + public void Connect(EndPoint remoteEP) + { + if (_isListening || !IsBound) + { + throw new InvalidOperationException(); + } + + if (remoteEP is not IPEndPoint) + { + throw new NotSupportedException(); + } + + IPEndPoint localEp = EnsureLocalEndpoint(true); + + _connecting = true; + + _proxy.RequestConnection(localEp, (IPEndPoint)remoteEP, ProtocolType); + + if (!Blocking && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + + _connectEvent.WaitOne(); //timeout? + + if (_connectResponse.Info.SourceIpV4 == 0) + { + throw new SocketException((int)WsaError.WSAECONNREFUSED); + } + + _connectResponse = default; + } + + public void HandleConnectResponse(ProxyConnectResponse obj) + { + if (!_connecting) + { + return; + } + + _connecting = false; + + if (_connectResponse.Info.SourceIpV4 != 0) + { + IPEndPoint remoteEp = GetEndpoint(obj.Info.SourceIpV4, obj.Info.SourcePort); + RemoteEndPoint = remoteEp; + + Connected = true; + } + else + { + // Connection failed + + SignalError(WsaError.WSAECONNREFUSED); + } + } + + public void Disconnect(bool reuseSocket) + { + if (Connected) + { + ConnectionEnded(); + + // The other side needs to be notified that connection ended. + _proxy.EndConnection(LocalEndPoint as IPEndPoint, RemoteEndPoint as IPEndPoint, ProtocolType); + } + } + + private void ConnectionEnded() + { + if (Connected) + { + RemoteEndPoint = null; + Connected = false; + } + } + + public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue) + { + if (optionLevel != SocketOptionLevel.Socket) + { + throw new NotImplementedException(); + } + + if (_socketOptions.TryGetValue(optionName, out int result)) + { + byte[] data = BitConverter.GetBytes(result); + Array.Copy(data, 0, optionValue, 0, Math.Min(data.Length, optionValue.Length)); + } + else + { + throw new NotImplementedException(); + } + } + + public void Listen(int backlog) + { + if (!IsBound) + { + throw new SocketException(); + } + + _isListening = true; + } + + public void HandleConnectRequest(ProxyConnectRequest obj) + { + lock (_connectRequests) + { + _connectRequests.Enqueue(obj); + } + + _connectEvent.Set(); + } + + public void HandleDisconnect(ProxyDisconnectMessage message) + { + Disconnect(false); + } + + public int Receive(Span buffer) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, SocketFlags.None, ref dummy); + } + + public int Receive(Span buffer, SocketFlags flags) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, flags, ref dummy); + } + + public int Receive(Span buffer, SocketFlags flags, out SocketError socketError) + { + EndPoint dummy = new IPEndPoint(IPAddress.Any, 0); + + return ReceiveFrom(buffer, flags, out socketError, ref dummy); + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEp) + { + // We just receive all packets meant for us anyways regardless of EP in the actual implementation. + // The point is mostly to return the endpoint that we got the data from. + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, ref remoteEp); + } + else if (_readShutdown) + { + return 0; + } + else if (!Blocking) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + int timeout = _receiveTimeout; + + _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout); + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, ref remoteEp); + } + else if (_readShutdown) + { + return 0; + } + else + { + throw new SocketException((int)WsaError.WSAETIMEDOUT); + } + } + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp) + { + // We just receive all packets meant for us anyways regardless of EP in the actual implementation. + // The point is mostly to return the endpoint that we got the data from. + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + socketError = SocketError.ConnectionReset; + return -1; + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp); + } + else if (_readShutdown) + { + socketError = SocketError.Success; + return 0; + } + else if (!Blocking) + { + throw new SocketException((int)WsaError.WSAEWOULDBLOCK); + } + } + + int timeout = _receiveTimeout; + + _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout); + + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + lock (_receiveQueue) + { + if (_receiveQueue.Count > 0) + { + return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp); + } + else if (_readShutdown) + { + socketError = SocketError.Success; + return 0; + } + else + { + socketError = SocketError.TimedOut; + return -1; + } + } + } + + private int ReceiveFromQueue(Span buffer, SocketFlags flags, ref EndPoint remoteEp) + { + int size = buffer.Length; + + // Assumes we have the receive queue lock, and at least one item in the queue. + ProxyDataPacket packet = _receiveQueue.Peek(); + + remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort); + + bool peek = (flags & SocketFlags.Peek) != 0; + + int read; + + if (packet.Data.Length > size) + { + read = size; + + // Cannot fit in the output buffer. Copy up to what we've got. + packet.Data.AsSpan(0, size).CopyTo(buffer); + + if (ProtocolType == ProtocolType.Udp) + { + // Udp overflows, loses the data, then throws an exception. + + if (!peek) + { + _receiveQueue.Dequeue(); + } + + throw new SocketException((int)WsaError.WSAEMSGSIZE); + } + else if (ProtocolType == ProtocolType.Tcp) + { + // Split the data at the buffer boundary. It will stay on the recieve queue. + + byte[] newData = new byte[packet.Data.Length - size]; + Array.Copy(packet.Data, size, newData, 0, newData.Length); + + packet.Data = newData; + } + } + else + { + read = packet.Data.Length; + + packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer); + + if (!peek) + { + _receiveQueue.Dequeue(); + } + } + + return read; + } + + private int ReceiveFromQueue(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp) + { + int size = buffer.Length; + + // Assumes we have the receive queue lock, and at least one item in the queue. + ProxyDataPacket packet = _receiveQueue.Peek(); + + remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort); + + bool peek = (flags & SocketFlags.Peek) != 0; + + int read; + + if (packet.Data.Length > size) + { + read = size; + + // Cannot fit in the output buffer. Copy up to what we've got. + packet.Data.AsSpan(0, size).CopyTo(buffer); + + if (ProtocolType == ProtocolType.Udp) + { + // Udp overflows, loses the data, then throws an exception. + + if (!peek) + { + _receiveQueue.Dequeue(); + } + + socketError = SocketError.MessageSize; + return -1; + } + else if (ProtocolType == ProtocolType.Tcp) + { + // Split the data at the buffer boundary. It will stay on the recieve queue. + + byte[] newData = new byte[packet.Data.Length - size]; + Array.Copy(packet.Data, size, newData, 0, newData.Length); + + packet.Data = newData; + } + } + else + { + read = packet.Data.Length; + + packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer); + + if (!peek) + { + _receiveQueue.Dequeue(); + } + } + + socketError = SocketError.Success; + + return read; + } + + public int Send(ReadOnlySpan buffer) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, SocketFlags.None, RemoteEndPoint); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, flags, RemoteEndPoint); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError) + { + // Send to the remote host chosen when we "connect" or "accept". + if (!Connected) + { + throw new SocketException(); + } + + return SendTo(buffer, flags, out socketError, RemoteEndPoint); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP) + { + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + throw new SocketException((int)WsaError.WSAECONNRESET); + } + + IPEndPoint localEp = EnsureLocalEndpoint(false); + + if (remoteEP is not IPEndPoint) + { + throw new NotSupportedException(); + } + + return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError, EndPoint remoteEP) + { + if (!Connected && ProtocolType == ProtocolType.Tcp) + { + socketError = SocketError.ConnectionReset; + return -1; + } + + IPEndPoint localEp = EnsureLocalEndpoint(false); + + if (remoteEP is not IPEndPoint) + { + // throw new NotSupportedException(); + socketError = SocketError.OperationNotSupported; + return -1; + } + + socketError = SocketError.Success; + + return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType); + } + + public bool Poll(int microSeconds, SelectMode mode) + { + return mode switch + { + SelectMode.SelectRead => Readable, + SelectMode.SelectWrite => Writable, + SelectMode.SelectError => Error, + _ => false + }; + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) + { + if (optionLevel != SocketOptionLevel.Socket) + { + throw new NotImplementedException(); + } + + switch (optionName) + { + case SocketOptionName.SendTimeout: + //_sendTimeout = optionValue; + break; + case SocketOptionName.ReceiveTimeout: + _receiveTimeout = optionValue; + break; + case SocketOptionName.Broadcast: + _broadcast = optionValue != 0; + break; + } + + lock (_socketOptions) + { + _socketOptions[optionName] = optionValue; + } + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue) + { + // Just linger uses this for now in BSD, which we ignore. + } + + public void Shutdown(SocketShutdown how) + { + switch (how) + { + case SocketShutdown.Both: + _readShutdown = true; + // _writeShutdown = true; + break; + case SocketShutdown.Receive: + _readShutdown = true; + break; + case SocketShutdown.Send: + // _writeShutdown = true; + break; + } + } + + public void ProxyDestroyed() + { + // Do nothing, for now. Will likely be more useful with TCP. + } + + public void Dispose() + { + + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs new file mode 100644 index 000000000..7da1aa998 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs @@ -0,0 +1,93 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; +using System.Net.Sockets; +using System.Threading; +using TcpClient = NetCoreServer.TcpClient; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxyClient : TcpClient, IProxyClient + { + private const int FailureTimeout = 4000; + + public ProxyConfig ProxyConfig { get; private set; } + + private readonly RyuLdnProtocol _protocol; + + private readonly ManualResetEvent _connected = new ManualResetEvent(false); + private readonly ManualResetEvent _ready = new ManualResetEvent(false); + private readonly AutoResetEvent _error = new AutoResetEvent(false); + + public P2pProxyClient(string address, int port) : base(address, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + _protocol = new RyuLdnProtocol(); + + _protocol.ProxyConfig += HandleProxyConfig; + + ConnectAsync(); + } + + protected override void OnConnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client connected a new session with Id {Id}"); + + _connected.Set(); + } + + protected override void OnDisconnected() + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client disconnected a session with Id {Id}"); + + SocketHelpers.UnregisterProxy(); + + _connected.Reset(); + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + _protocol.Read(buffer, (int)offset, (int)size); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client caught an error with code {error}"); + + _error.Set(); + } + + private void HandleProxyConfig(LdnHeader header, ProxyConfig config) + { + ProxyConfig = config; + + SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol)); + + _ready.Set(); + } + + public bool EnsureProxyReady() + { + return _ready.WaitOne(FailureTimeout); + } + + public bool PerformAuth(ExternalProxyConfig config) + { + bool signalled = _connected.WaitOne(FailureTimeout); + + if (!signalled) + { + return false; + } + + SendAsync(_protocol.Encode(PacketId.ExternalProxy, config)); + + return true; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs new file mode 100644 index 000000000..598fb654f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs @@ -0,0 +1,388 @@ +using NetCoreServer; +using Open.Nat; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxyServer : TcpServer, IDisposable + { + public const ushort PrivatePortBase = 39990; + public const int PrivatePortRange = 10; + + private const ushort PublicPortBase = 39990; + private const int PublicPortRange = 10; + + private const ushort PortLeaseLength = 60; + private const ushort PortLeaseRenew = 50; + + private const ushort AuthWaitSeconds = 1; + + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + public ushort PrivatePort { get; } + + private ushort _publicPort; + + private bool _disposed; + private readonly CancellationTokenSource _disposedCancellation = new CancellationTokenSource(); + + private NatDevice _natDevice; + private Mapping _portMapping; + + private readonly List _players = new List(); + + private readonly List _waitingTokens = new List(); + private readonly AutoResetEvent _tokenEvent = new AutoResetEvent(false); + + private uint _broadcastAddress; + + private readonly LdnMasterProxyClient _master; + private readonly RyuLdnProtocol _masterProtocol; + private readonly RyuLdnProtocol _protocol; + + public P2pProxyServer(LdnMasterProxyClient master, ushort port, RyuLdnProtocol masterProtocol) : base(IPAddress.Any, port) + { + if (ProxyHelpers.SupportsNoDelay()) + { + OptionNoDelay = true; + } + + PrivatePort = port; + + _master = master; + _masterProtocol = masterProtocol; + + _masterProtocol.ExternalProxyState += HandleStateChange; + _masterProtocol.ExternalProxyToken += HandleToken; + + _protocol = new RyuLdnProtocol(); + } + + private void HandleToken(LdnHeader header, ExternalProxyToken token) + { + _lock.EnterWriteLock(); + + _waitingTokens.Add(token); + + _lock.ExitWriteLock(); + + _tokenEvent.Set(); + } + + private void HandleStateChange(LdnHeader header, ExternalProxyConnectionState state) + { + if (!state.Connected) + { + _lock.EnterWriteLock(); + + _waitingTokens.RemoveAll(token => token.VirtualIp == state.IpAddress); + + _players.RemoveAll(player => + { + if (player.VirtualIpAddress == state.IpAddress) + { + player.DisconnectAndStop(); + + return true; + } + + return false; + }); + + _lock.ExitWriteLock(); + } + } + + public void Configure(ProxyConfig config) + { + _broadcastAddress = config.ProxyIp | (~config.ProxySubnetMask); + } + + public async Task NatPunch() + { + NatDiscoverer discoverer = new NatDiscoverer(); + CancellationTokenSource cts = new CancellationTokenSource(1000); + + NatDevice device; + + try + { + device = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, cts); + } + catch (NatDeviceNotFoundException) + { + return 0; + } + + _publicPort = PublicPortBase; + + for (int i = 0; i < PublicPortRange; i++) + { + try + { + _portMapping = new Mapping(Protocol.Tcp, PrivatePort, _publicPort, PortLeaseLength, "Ryujinx Local Multiplayer"); + + await device.CreatePortMapAsync(_portMapping); + + break; + } + catch (MappingException) + { + _publicPort++; + } + catch (Exception) + { + return 0; + } + + if (i == PublicPortRange - 1) + { + _publicPort = 0; + } + } + + if (_publicPort != 0) + { + _ = Task.Delay(PortLeaseRenew * 1000, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + } + + _natDevice = device; + + return _publicPort; + } + + // Proxy handlers + + private void RouteMessage(P2pProxySession sender, ref ProxyInfo info, Action action) + { + if (info.SourceIpV4 == 0) + { + // If they sent from a connection bound on 0.0.0.0, make others see it as them. + info.SourceIpV4 = sender.VirtualIpAddress; + } + else if (info.SourceIpV4 != sender.VirtualIpAddress) + { + // Can't pretend to be somebody else. + return; + } + + uint destIp = info.DestIpV4; + + if (destIp == 0xc0a800ff) + { + destIp = _broadcastAddress; + } + + bool isBroadcast = destIp == _broadcastAddress; + + _lock.EnterReadLock(); + + if (isBroadcast) + { + _players.ForEach(player => + { + action(player); + }); + } + else + { + P2pProxySession target = _players.FirstOrDefault(player => player.VirtualIpAddress == destIp); + + if (target != null) + { + action(target); + } + } + + _lock.ExitReadLock(); + } + + public void HandleProxyDisconnect(P2pProxySession sender, LdnHeader header, ProxyDisconnectMessage message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyDisconnect, message)); + }); + } + + public void HandleProxyData(P2pProxySession sender, LdnHeader header, ProxyDataHeader message, byte[] data) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyData, message, data)); + }); + } + + public void HandleProxyConnectReply(P2pProxySession sender, LdnHeader header, ProxyConnectResponse message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnectReply, message)); + }); + } + + public void HandleProxyConnect(P2pProxySession sender, LdnHeader header, ProxyConnectRequest message) + { + RouteMessage(sender, ref message.Info, (target) => + { + target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnect, message)); + }); + } + + // End proxy handlers + + private async Task RefreshLease() + { + if (_disposed || _natDevice == null) + { + return; + } + + try + { + await _natDevice.CreatePortMapAsync(_portMapping); + } + catch (Exception) + { + + } + + _ = Task.Delay(PortLeaseRenew, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + } + + public bool TryRegisterUser(P2pProxySession session, ExternalProxyConfig config) + { + _lock.EnterWriteLock(); + + // Attempt to find matching configuration. If we don't find one, wait for a bit and try again. + // Woken by new tokens coming in from the master server. + + IPAddress address = (session.Socket.RemoteEndPoint as IPEndPoint).Address; + byte[] addressBytes = ProxyHelpers.AddressTo16Byte(address); + + long time; + long endTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency * AuthWaitSeconds; + + do + { + for (int i = 0; i < _waitingTokens.Count; i++) + { + ExternalProxyToken waitToken = _waitingTokens[i]; + + // Allow any client that has a private IP to connect. (indicated by the server as all 0 in the token) + + bool isPrivate = waitToken.PhysicalIp.AsSpan().SequenceEqual(new byte[16]); + bool ipEqual = isPrivate || waitToken.AddressFamily == address.AddressFamily && waitToken.PhysicalIp.AsSpan().SequenceEqual(addressBytes); + + if (ipEqual && waitToken.Token.AsSpan().SequenceEqual(config.Token.AsSpan())) + { + // This is a match. + + _waitingTokens.RemoveAt(i); + + session.SetIpv4(waitToken.VirtualIp); + + ProxyConfig pconfig = new ProxyConfig + { + ProxyIp = session.VirtualIpAddress, + ProxySubnetMask = 0xFFFF0000 // TODO: Use from server. + }; + + if (_players.Count == 0) + { + Configure(pconfig); + } + + _players.Add(session); + + session.SendAsync(_protocol.Encode(PacketId.ProxyConfig, pconfig)); + + _lock.ExitWriteLock(); + + return true; + } + } + + // Couldn't find the token. + // It may not have arrived yet, so wait for one to arrive. + + _lock.ExitWriteLock(); + + time = Stopwatch.GetTimestamp(); + int remainingMs = (int)((endTime - time) / (Stopwatch.Frequency / 1000)); + + if (remainingMs < 0) + { + remainingMs = 0; + } + + _tokenEvent.WaitOne(remainingMs); + + _lock.EnterWriteLock(); + + } while (time < endTime); + + _lock.ExitWriteLock(); + + return false; + } + + public void DisconnectProxyClient(P2pProxySession session) + { + _lock.EnterWriteLock(); + + bool removed = _players.Remove(session); + + if (removed) + { + _master.SendAsync(_masterProtocol.Encode(PacketId.ExternalProxyState, new ExternalProxyConnectionState + { + IpAddress = session.VirtualIpAddress, + Connected = false + })); + } + + _lock.ExitWriteLock(); + } + + public new void Dispose() + { + base.Dispose(); + + _disposed = true; + _disposedCancellation.Cancel(); + + try + { + Task delete = _natDevice?.DeletePortMapAsync(new Mapping(Protocol.Tcp, PrivatePort, _publicPort, 60, "Ryujinx Local Multiplayer")); + + // Just absorb any exceptions. + delete?.ContinueWith((task) => { }); + } + catch (Exception) + { + // Fail silently. + } + } + + protected override TcpSession CreateSession() + { + return new P2pProxySession(this); + } + + protected override void OnError(SocketError error) + { + Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP server caught an error with code {error}"); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs new file mode 100644 index 000000000..515feeac5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs @@ -0,0 +1,90 @@ +using NetCoreServer; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using System; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + class P2pProxySession : TcpSession + { + public uint VirtualIpAddress { get; private set; } + public RyuLdnProtocol Protocol { get; } + + private readonly P2pProxyServer _parent; + + private bool _masterClosed; + + public P2pProxySession(P2pProxyServer server) : base(server) + { + _parent = server; + + Protocol = new RyuLdnProtocol(); + + Protocol.ProxyDisconnect += HandleProxyDisconnect; + Protocol.ProxyData += HandleProxyData; + Protocol.ProxyConnectReply += HandleProxyConnectReply; + Protocol.ProxyConnect += HandleProxyConnect; + + Protocol.ExternalProxy += HandleAuthentication; + } + + private void HandleAuthentication(LdnHeader header, ExternalProxyConfig token) + { + if (!_parent.TryRegisterUser(this, token)) + { + Disconnect(); + } + } + + public void SetIpv4(uint ip) + { + VirtualIpAddress = ip; + } + + public void DisconnectAndStop() + { + _masterClosed = true; + + Disconnect(); + } + + protected override void OnDisconnected() + { + if (!_masterClosed) + { + _parent.DisconnectProxyClient(this); + } + } + + protected override void OnReceived(byte[] buffer, long offset, long size) + { + try + { + Protocol.Read(buffer, (int)offset, (int)size); + } + catch (Exception) + { + Disconnect(); + } + } + + private void HandleProxyDisconnect(LdnHeader header, ProxyDisconnectMessage message) + { + _parent.HandleProxyDisconnect(this, header, message); + } + + private void HandleProxyData(LdnHeader header, ProxyDataHeader message, byte[] data) + { + _parent.HandleProxyData(this, header, message, data); + } + + private void HandleProxyConnectReply(LdnHeader header, ProxyConnectResponse data) + { + _parent.HandleProxyConnectReply(this, header, data); + } + + private void HandleProxyConnect(LdnHeader header, ProxyConnectRequest message) + { + _parent.HandleProxyConnect(this, header, message); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs new file mode 100644 index 000000000..42b1ab6a2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy +{ + static class ProxyHelpers + { + public static byte[] AddressTo16Byte(IPAddress address) + { + byte[] ipBytes = new byte[16]; + byte[] srcBytes = address.GetAddressBytes(); + + Array.Copy(srcBytes, 0, ipBytes, 0, srcBytes.Length); + + return ipBytes; + } + + public static bool SupportsNoDelay() + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs new file mode 100644 index 000000000..d0eeaf125 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs @@ -0,0 +1,380 @@ +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu +{ + class RyuLdnProtocol + { + private const byte CurrentProtocolVersion = 1; + private const int Magic = ('R' << 0) | ('L' << 8) | ('D' << 16) | ('N' << 24); + private const int MaxPacketSize = 131072; + + private readonly int _headerSize = Marshal.SizeOf(); + + private readonly byte[] _buffer = new byte[MaxPacketSize]; + private int _bufferEnd = 0; + + // Client Packets. + public event Action Initialize; + public event Action Passphrase; + public event Action Connected; + public event Action SyncNetwork; + public event Action ScanReply; + public event Action ScanReplyEnd; + public event Action Disconnected; + + // External Proxy Packets. + public event Action ExternalProxy; + public event Action ExternalProxyState; + public event Action ExternalProxyToken; + + // Server Packets. + public event Action CreateAccessPoint; + public event Action CreateAccessPointPrivate; + public event Action Reject; + public event Action RejectReply; + public event Action SetAcceptPolicy; + public event Action SetAdvertiseData; + public event Action Connect; + public event Action ConnectPrivate; + public event Action Scan; + + // Proxy Packets. + public event Action ProxyConfig; + public event Action ProxyConnect; + public event Action ProxyConnectReply; + public event Action ProxyData; + public event Action ProxyDisconnect; + + // Lifecycle Packets. + public event Action NetworkError; + public event Action Ping; + + public RyuLdnProtocol() { } + + public void Reset() + { + _bufferEnd = 0; + } + + public void Read(byte[] data, int offset, int size) + { + int index = 0; + + while (index < size) + { + if (_bufferEnd < _headerSize) + { + // Assemble the header first. + + int copyable = Math.Min(size - index, Math.Min(size, _headerSize - _bufferEnd)); + + Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable); + + index += copyable; + _bufferEnd += copyable; + } + + if (_bufferEnd >= _headerSize) + { + // The header is available. Make sure we received all the data (size specified in the header) + + LdnHeader ldnHeader = MemoryMarshal.Cast(_buffer)[0]; + + if (ldnHeader.Magic != Magic) + { + throw new InvalidOperationException("Invalid magic number in received packet."); + } + + if (ldnHeader.Version != CurrentProtocolVersion) + { + throw new InvalidOperationException($"Protocol version mismatch. Expected ${CurrentProtocolVersion}, was ${ldnHeader.Version}."); + } + + int finalSize = _headerSize + ldnHeader.DataSize; + + if (finalSize >= MaxPacketSize) + { + throw new InvalidOperationException($"Max packet size {MaxPacketSize} exceeded."); + } + + int copyable = Math.Min(size - index, Math.Min(size, finalSize - _bufferEnd)); + + Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable); + + index += copyable; + _bufferEnd += copyable; + + if (finalSize == _bufferEnd) + { + // The full packet has been retrieved. Send it to be decoded. + + byte[] ldnData = new byte[ldnHeader.DataSize]; + + Array.Copy(_buffer, _headerSize, ldnData, 0, ldnData.Length); + + DecodeAndHandle(ldnHeader, ldnData); + + Reset(); + } + } + } + } + + private (T, byte[]) ParseWithData(byte[] data) where T : struct + { + T str = default; + int size = Marshal.SizeOf(str); + + byte[] remainder = new byte[data.Length - size]; + + if (remainder.Length > 0) + { + Array.Copy(data, size, remainder, 0, remainder.Length); + } + + return (MemoryMarshal.Read(data), remainder); + } + + private void DecodeAndHandle(LdnHeader header, byte[] data) + { + switch ((PacketId)header.Type) + { + // Client Packets. + case PacketId.Initialize: + { + Initialize?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Passphrase: + { + Passphrase?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Connected: + { + Connected?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.SyncNetwork: + { + SyncNetwork?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ScanReply: + { + ScanReply?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + case PacketId.ScanReplyEnd: + { + ScanReplyEnd?.Invoke(header); + + break; + } + case PacketId.Disconnect: + { + Disconnected?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // External Proxy Packets. + case PacketId.ExternalProxy: + { + ExternalProxy?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ExternalProxyState: + { + ExternalProxyState?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ExternalProxyToken: + { + ExternalProxyToken?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Server Packets. + case PacketId.CreateAccessPoint: + { + (CreateAccessPointRequest packet, byte[] extraData) = ParseWithData(data); + CreateAccessPoint?.Invoke(header, packet, extraData); + break; + } + case PacketId.CreateAccessPointPrivate: + { + (CreateAccessPointPrivateRequest packet, byte[] extraData) = ParseWithData(data); + CreateAccessPointPrivate?.Invoke(header, packet, extraData); + break; + } + case PacketId.Reject: + { + Reject?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.RejectReply: + { + RejectReply?.Invoke(header); + + break; + } + case PacketId.SetAcceptPolicy: + { + SetAcceptPolicy?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.SetAdvertiseData: + { + SetAdvertiseData?.Invoke(header, data); + + break; + } + case PacketId.Connect: + { + Connect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ConnectPrivate: + { + ConnectPrivate?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.Scan: + { + Scan?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Proxy Packets + case PacketId.ProxyConfig: + { + ProxyConfig?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyConnect: + { + ProxyConnect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyConnectReply: + { + ProxyConnectReply?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.ProxyData: + { + (ProxyDataHeader packet, byte[] extraData) = ParseWithData(data); + + ProxyData?.Invoke(header, packet, extraData); + + break; + } + case PacketId.ProxyDisconnect: + { + ProxyDisconnect?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + // Lifecycle Packets. + case PacketId.Ping: + { + Ping?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + case PacketId.NetworkError: + { + NetworkError?.Invoke(header, MemoryMarshal.Read(data)); + + break; + } + + default: + break; + } + } + + private static LdnHeader GetHeader(PacketId type, int dataSize) + { + return new LdnHeader() + { + Magic = Magic, + Version = CurrentProtocolVersion, + Type = (byte)type, + DataSize = dataSize + }; + } + + public byte[] Encode(PacketId type) + { + LdnHeader header = GetHeader(type, 0); + + return SpanHelpers.AsSpan(ref header).ToArray(); + } + + public byte[] Encode(PacketId type, byte[] data) + { + LdnHeader header = GetHeader(type, data.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + data.Length); + Array.Copy(data, 0, result, Marshal.SizeOf(), data.Length); + + return result; + } + + public byte[] Encode(PacketId type, T packet) where T : unmanaged + { + byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray(); + + LdnHeader header = GetHeader(type, packetData.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + packetData.Length); + Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length); + + return result; + } + + public byte[] Encode(PacketId type, T packet, byte[] data) where T : unmanaged + { + byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray(); + + LdnHeader header = GetHeader(type, packetData.Length + data.Length); + + byte[] result = SpanHelpers.AsSpan(ref header).ToArray(); + + Array.Resize(ref result, result.Length + packetData.Length + data.Length); + Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length); + Array.Copy(data, 0, result, Marshal.SizeOf() + packetData.Length, data.Length); + + return result; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs new file mode 100644 index 000000000..448d33f29 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x4)] + struct DisconnectMessage + { + public uint DisconnectIP; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs new file mode 100644 index 000000000..9cbb80242 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs @@ -0,0 +1,19 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Sent by the server to point a client towards an external server being used as a proxy. + /// The client then forwards this to the external proxy after connecting, to verify the connection worked. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x26, Pack = 1)] + struct ExternalProxyConfig + { + public Array16 ProxyIp; + public AddressFamily AddressFamily; + public ushort ProxyPort; + public Array16 Token; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs new file mode 100644 index 000000000..ecf4e14f7 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Indicates a change in connection state for the given client. + /// Is sent to notify the master server when connection is first established. + /// Can be sent by the external proxy to the master server to notify it of a proxy disconnect. + /// Can be sent by the master server to notify the external proxy of a user leaving a room. + /// Both will result in a force kick. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 4)] + struct ExternalProxyConnectionState + { + public uint IpAddress; + public bool Connected; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs new file mode 100644 index 000000000..0a8980c37 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs @@ -0,0 +1,20 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Sent by the master server to an external proxy to tell them someone is going to connect. + /// This drives authentication, and lets the proxy know what virtual IP to give to each joiner, + /// as these are managed by the master server. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x28)] + struct ExternalProxyToken + { + public uint VirtualIp; + public Array16 Token; + public Array16 PhysicalIp; + public AddressFamily AddressFamily; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs new file mode 100644 index 000000000..36ddc65fe --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs @@ -0,0 +1,20 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// This message is first sent by the client to identify themselves. + /// If the server has a token+mac combo that matches the submission, then they are returned their new ID and mac address. (the mac is also reassigned to the new id) + /// Otherwise, they are returned a random mac address. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x16)] + struct InitializeMessage + { + // All 0 if we don't have an ID yet. + public Array16 Id; + + // All 0 if we don't have a mac yet. + public Array6 MacAddress; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs new file mode 100644 index 000000000..f41f15ab4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0xA)] + struct LdnHeader + { + public uint Magic; + public byte Type; + public byte Version; + public int DataSize; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs new file mode 100644 index 000000000..b8ef5fbc1 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs @@ -0,0 +1,36 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + enum PacketId + { + Initialize, + Passphrase, + + CreateAccessPoint, + CreateAccessPointPrivate, + ExternalProxy, + ExternalProxyToken, + ExternalProxyState, + SyncNetwork, + Reject, + RejectReply, + Scan, + ScanReply, + ScanReplyEnd, + Connect, + ConnectPrivate, + Connected, + Disconnect, + + ProxyConfig, + ProxyConnect, + ProxyConnectReply, + ProxyData, + ProxyDisconnect, + + SetAcceptPolicy, + SetAdvertiseData, + + Ping = 254, + NetworkError = 255 + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs new file mode 100644 index 000000000..0deba0b07 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs @@ -0,0 +1,11 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x80)] + struct PassphraseMessage + { + public Array128 Passphrase; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs new file mode 100644 index 000000000..135e39caa --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x2)] + struct PingMessage + { + public byte Requester; + public byte Id; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs new file mode 100644 index 000000000..ffce77791 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct ProxyConnectRequest + { + public ProxyInfo Info; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs new file mode 100644 index 000000000..de2e430fb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct ProxyConnectResponse + { + public ProxyInfo Info; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs new file mode 100644 index 000000000..e46a40692 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Represents data sent over a transport layer. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x14)] + struct ProxyDataHeader + { + public ProxyInfo Info; + public uint DataLength; // Followed by the data with the specified byte length. + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs new file mode 100644 index 000000000..eb3648413 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + class ProxyDataPacket + { + public ProxyDataHeader Header; + public byte[] Data; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs new file mode 100644 index 000000000..2154ae109 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x14)] + struct ProxyDisconnectMessage + { + public ProxyInfo Info; + public int DisconnectReason; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs new file mode 100644 index 000000000..d9338f244 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs @@ -0,0 +1,20 @@ +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + /// + /// Information included in all proxied communication. + /// + [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 1)] + struct ProxyInfo + { + public uint SourceIpV4; + public ushort SourcePort; + + public uint DestIpV4; + public ushort DestPort; + + public ProtocolType Protocol; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs new file mode 100644 index 000000000..1c2ce1f8b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs @@ -0,0 +1,18 @@ +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8)] + struct RejectRequest + { + public uint NodeId; + public DisconnectReason DisconnectReason; + + public RejectRequest(DisconnectReason disconnectReason, uint nodeId) + { + DisconnectReason = disconnectReason; + NodeId = nodeId; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs new file mode 100644 index 000000000..f3bd72023 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs @@ -0,0 +1,23 @@ +using Ryujinx.Common.Memory; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x28, Pack = 1)] + struct RyuNetworkConfig + { + public Array16 GameVersion; + + // PrivateIp is included for external proxies for the case where a client attempts to join from + // their own LAN. UPnP forwarding can fail when connecting devices on the same network over the public IP, + // so if their public IP is identical, the internal address should be sent instead. + + // The fields below are 0 if not hosting a p2p proxy. + + public Array16 PrivateIp; + public AddressFamily AddressFamily; + public ushort ExternalProxyPort; + public ushort InternalProxyPort; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs new file mode 100644 index 000000000..c4a969901 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs @@ -0,0 +1,11 @@ +using Ryujinx.HLE.HOS.Services.Ldn.Types; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x1, Pack = 1)] + struct SetAcceptPolicyRequest + { + public AcceptPolicy StationAcceptPolicy; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index e39c01978..fa43f789e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -14,6 +14,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public bool Connected { get; private set; } + public ProxyConfig Config => _parent.NetworkClient.Config; + public Station(IUserLocalCommunicationService parent) { _parent = parent; @@ -48,9 +50,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator public void Dispose() { - _parent.NetworkClient.DisconnectNetwork(); + if (_parent.NetworkClient != null) + { + _parent.NetworkClient.DisconnectNetwork(); - _parent.NetworkClient.NetworkChange -= NetworkChanged; + _parent.NetworkClient.NetworkChange -= NetworkChanged; + } } private ResultCode NetworkErrorToResult(NetworkError error) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs index ac0ff7d94..0972c21c0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types @@ -14,5 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types public UserConfig UserConfig; public NetworkConfig NetworkConfig; public AddressList AddressList; + + public RyuNetworkConfig RyuNetworkConfig; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs index f67f0aac9..d2dc5b698 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Services.Ldn.Types; +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types; using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types @@ -6,11 +7,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types /// /// Advertise data is appended separately (remaining data in the buffer). /// - [StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)] + [StructLayout(LayoutKind.Sequential, Size = 0xBC, Pack = 1)] struct CreateAccessPointRequest { public SecurityConfig SecurityConfig; public UserConfig UserConfig; public NetworkConfig NetworkConfig; + + public RyuNetworkConfig RyuNetworkConfig; } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs new file mode 100644 index 000000000..c89c08bbe --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x8)] + struct ProxyConfig + { + public uint ProxyIp; + public uint ProxySubnetMask; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs index 65d380979..e1db98e5f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs @@ -8,6 +8,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager public uint FileVersion { get; set; } public byte[] TagUuid { get; set; } public string AmiiboId { get; set; } + public string NickName { get; set; } public DateTime FirstWriteDate { get; set; } public DateTime LastWriteDate { get; set; } public ushort WriteCounter { get; set; } diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs index ba4a81e0e..0c685471c 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs @@ -64,16 +64,17 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp }; } - public static RegisterInfo GetRegisterInfo(ITickSource tickSource, string amiiboId, string nickname) + public static RegisterInfo GetRegisterInfo(ITickSource tickSource, string amiiboId, string userName) { VirtualAmiiboFile amiiboFile = LoadAmiiboFile(amiiboId); - + string nickname = amiiboFile.NickName ?? "Ryujinx"; UtilityImpl utilityImpl = new(tickSource); CharInfo charInfo = new(); charInfo.SetFromStoreData(StoreData.BuildDefault(utilityImpl, 0)); - charInfo.Nickname = Nickname.FromString(nickname); + // This is the player's name + charInfo.Nickname = Nickname.FromString(userName); RegisterInfo registerInfo = new() { @@ -85,11 +86,20 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp Reserved1 = new Array64(), Reserved2 = new Array58(), }; - "Ryujinx"u8.CopyTo(registerInfo.Nickname.AsSpan()); + // This is the amiibo's name + byte[] nicknameBytes = System.Text.Encoding.UTF8.GetBytes(nickname); + nicknameBytes.CopyTo(registerInfo.Nickname.AsSpan()); return registerInfo; } + public static void UpdateNickName(string amiiboId, string newNickName) + { + VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId); + virtualAmiiboFile.NickName = newNickName; + SaveAmiiboFile(virtualAmiiboFile); + } + public static bool OpenApplicationArea(string amiiboId, uint applicationAreaId) { VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId); diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs index 21d48288e..3a40a4ac5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs @@ -95,7 +95,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd } } - ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol) + ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol, context.Device.Configuration.MultiplayerLanInterfaceId) { Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking), }; diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs index c9b811cf5..981fe0a8f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types; using System; using System.Collections.Generic; @@ -21,21 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; } - public nint Handle => Socket.Handle; + public nint Handle => IntPtr.Zero; public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint; public IPEndPoint LocalEndPoint => Socket.LocalEndPoint as IPEndPoint; - public Socket Socket { get; } + public ISocketImpl Socket { get; } - public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType) + public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, string lanInterfaceId) { - Socket = new Socket(addressFamily, socketType, protocolType); + Socket = SocketHelpers.CreateSocket(addressFamily, socketType, protocolType, lanInterfaceId); Refcount = 1; } - private ManagedSocket(Socket socket) + private ManagedSocket(ISocketImpl socket) { Socket = socket; Refcount = 1; @@ -185,6 +186,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl } } + bool hasEmittedBlockingWarning = false; + public LinuxError Receive(out int receiveSize, Span buffer, BsdSocketFlags flags) { LinuxError result; @@ -199,6 +202,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl shouldBlockAfterOperation = true; } + if (Blocking && !hasEmittedBlockingWarning) + { + Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); + hasEmittedBlockingWarning = true; + } + receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags)); result = LinuxError.SUCCESS; @@ -236,6 +245,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl shouldBlockAfterOperation = true; } + if (Blocking && !hasEmittedBlockingWarning) + { + Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors."); + hasEmittedBlockingWarning = true; + } + if (!Socket.IsBound) { receiveSize = -1; @@ -313,7 +328,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported GetSockOpt Option: {option} Level: {level}"); optionValue.Clear(); - return LinuxError.SUCCESS; + return LinuxError.EOPNOTSUPP; } byte[] tempOptionValue = new byte[optionValue.Length]; @@ -347,7 +362,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl { Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported SetSockOpt Option: {option} Level: {level}"); - return LinuxError.SUCCESS; + return LinuxError.EOPNOTSUPP; } int value = optionValue.Length >= 4 ? MemoryMarshal.Read(optionValue) : MemoryMarshal.Read(optionValue); @@ -493,7 +508,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl try { - int receiveSize = Socket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); + int receiveSize = (Socket as DefaultSocket).BaseSocket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); if (receiveSize > 0) { @@ -531,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl try { - int sendSize = Socket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); + int sendSize = (Socket as DefaultSocket).BaseSocket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError); if (sendSize > 0) { diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs index d0db44086..e870e8aea 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs @@ -1,4 +1,5 @@ using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types; using System.Collections.Generic; using System.Net.Sockets; @@ -26,45 +27,46 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public LinuxError Poll(List events, int timeoutMilliseconds, out int updatedCount) { - List readEvents = new(); - List writeEvents = new(); - List errorEvents = new(); + List readEvents = new(); + List writeEvents = new(); + List errorEvents = new(); updatedCount = 0; foreach (PollEvent evnt in events) { - ManagedSocket socket = (ManagedSocket)evnt.FileDescriptor; - - bool isValidEvent = evnt.Data.InputEvents == 0; - - errorEvents.Add(socket.Socket); - - if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + if (evnt.FileDescriptor is ManagedSocket ms) { - readEvents.Add(socket.Socket); + bool isValidEvent = evnt.Data.InputEvents == 0; - isValidEvent = true; - } + errorEvents.Add(ms.Socket); - if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0) - { - readEvents.Add(socket.Socket); + if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + { + readEvents.Add(ms.Socket); - isValidEvent = true; - } + isValidEvent = true; + } - if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0) - { - writeEvents.Add(socket.Socket); + if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0) + { + readEvents.Add(ms.Socket); - isValidEvent = true; - } + isValidEvent = true; + } - if (!isValidEvent) - { - Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}"); - return LinuxError.EINVAL; + if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0) + { + writeEvents.Add(ms.Socket); + + isValidEvent = true; + } + + if (!isValidEvent) + { + Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}"); + return LinuxError.EINVAL; + } } } @@ -72,7 +74,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl { int actualTimeoutMicroseconds = timeoutMilliseconds == -1 ? -1 : timeoutMilliseconds * 1000; - Socket.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds); + SocketHelpers.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds); } catch (SocketException exception) { @@ -81,34 +83,37 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl foreach (PollEvent evnt in events) { - Socket socket = ((ManagedSocket)evnt.FileDescriptor).Socket; - - PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents; - - if (errorEvents.Contains(socket)) + if (evnt.FileDescriptor is ManagedSocket ms) { - outputEvents |= PollEventTypeMask.Error; + ISocketImpl socket = ms.Socket; - if (!socket.Connected || !socket.IsBound) + PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents; + + if (errorEvents.Contains(ms.Socket)) { - outputEvents |= PollEventTypeMask.Disconnected; - } - } + outputEvents |= PollEventTypeMask.Error; - if (readEvents.Contains(socket)) - { - if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + if (!socket.Connected || !socket.IsBound) + { + outputEvents |= PollEventTypeMask.Disconnected; + } + } + + if (readEvents.Contains(ms.Socket)) { - outputEvents |= PollEventTypeMask.Input; + if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0) + { + outputEvents |= PollEventTypeMask.Input; + } } - } - if (writeEvents.Contains(socket)) - { - outputEvents |= PollEventTypeMask.Output; - } + if (writeEvents.Contains(ms.Socket)) + { + outputEvents |= PollEventTypeMask.Output; + } - evnt.Data.OutputEvents = outputEvents; + evnt.Data.OutputEvents = outputEvents; + } } updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count; @@ -118,53 +123,55 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl public LinuxError Select(List events, int timeout, out int updatedCount) { - List readEvents = new(); - List writeEvents = new(); - List errorEvents = new(); + List readEvents = new(); + List writeEvents = new(); + List errorEvents = new(); updatedCount = 0; foreach (PollEvent pollEvent in events) { - ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor; - - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input)) + if (pollEvent.FileDescriptor is ManagedSocket ms) { - readEvents.Add(socket.Socket); - } + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input)) + { + readEvents.Add(ms.Socket); + } - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output)) - { - writeEvents.Add(socket.Socket); - } + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output)) + { + writeEvents.Add(ms.Socket); + } - if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error)) - { - errorEvents.Add(socket.Socket); + if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error)) + { + errorEvents.Add(ms.Socket); + } } } - Socket.Select(readEvents, writeEvents, errorEvents, timeout); + SocketHelpers.Select(readEvents, writeEvents, errorEvents, timeout); updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count; foreach (PollEvent pollEvent in events) { - ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor; - - if (readEvents.Contains(socket.Socket)) + if (pollEvent.FileDescriptor is ManagedSocket ms) { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Input; - } + if (readEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Input; + } - if (writeEvents.Contains(socket.Socket)) - { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Output; - } + if (writeEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Output; + } - if (errorEvents.Contains(socket.Socket)) - { - pollEvent.Data.OutputEvents |= PollEventTypeMask.Error; + if (errorEvents.Contains(ms.Socket)) + { + pollEvent.Data.OutputEvents |= PollEventTypeMask.Error; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs new file mode 100644 index 000000000..f1040e799 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs @@ -0,0 +1,178 @@ +using Ryujinx.Common.Utilities; +using System; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + class DefaultSocket : ISocketImpl + { + public Socket BaseSocket { get; } + + public EndPoint RemoteEndPoint => BaseSocket.RemoteEndPoint; + + public EndPoint LocalEndPoint => BaseSocket.LocalEndPoint; + + public bool Connected => BaseSocket.Connected; + + public bool IsBound => BaseSocket.IsBound; + + public AddressFamily AddressFamily => BaseSocket.AddressFamily; + + public SocketType SocketType => BaseSocket.SocketType; + + public ProtocolType ProtocolType => BaseSocket.ProtocolType; + + public bool Blocking { get => BaseSocket.Blocking; set => BaseSocket.Blocking = value; } + + public int Available => BaseSocket.Available; + + private readonly string _lanInterfaceId; + + public DefaultSocket(Socket baseSocket, string lanInterfaceId) + { + _lanInterfaceId = lanInterfaceId; + + BaseSocket = baseSocket; + } + + public DefaultSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId) + { + _lanInterfaceId = lanInterfaceId; + + BaseSocket = new Socket(domain, type, protocol); + } + + private void EnsureNetworkInterfaceBound() + { + if (_lanInterfaceId != "0" && !BaseSocket.IsBound) + { + (_, UnicastIPAddressInformation ipInfo) = NetworkHelpers.GetLocalInterface(_lanInterfaceId); + + BaseSocket.Bind(new IPEndPoint(ipInfo.Address, 0)); + } + } + + public ISocketImpl Accept() + { + return new DefaultSocket(BaseSocket.Accept(), _lanInterfaceId); + } + + public void Bind(EndPoint localEP) + { + // NOTE: The guest is able to receive on 0.0.0.0 without it being limited to the chosen network interface. + // This is because it must get loopback traffic as well. This could allow other network traffic to leak in. + + BaseSocket.Bind(localEP); + } + + public void Close() + { + BaseSocket.Close(); + } + + public void Connect(EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + BaseSocket.Connect(remoteEP); + } + + public void Disconnect(bool reuseSocket) + { + BaseSocket.Disconnect(reuseSocket); + } + + public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue) + { + BaseSocket.GetSocketOption(optionLevel, optionName, optionValue); + } + + public void Listen(int backlog) + { + BaseSocket.Listen(backlog); + } + + public int Receive(Span buffer) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer); + } + + public int Receive(Span buffer, SocketFlags flags) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer, flags); + } + + public int Receive(Span buffer, SocketFlags flags, out SocketError socketError) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Receive(buffer, flags, out socketError); + } + + public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.ReceiveFrom(buffer, flags, ref remoteEP); + } + + public int Send(ReadOnlySpan buffer) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer, flags); + } + + public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.Send(buffer, flags, out socketError); + } + + public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP) + { + EnsureNetworkInterfaceBound(); + + return BaseSocket.SendTo(buffer, flags, remoteEP); + } + + public bool Poll(int microSeconds, SelectMode mode) + { + return BaseSocket.Poll(microSeconds, mode); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue) + { + BaseSocket.SetSocketOption(optionLevel, optionName, optionValue); + } + + public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue) + { + BaseSocket.SetSocketOption(optionLevel, optionName, optionValue); + } + + public void Shutdown(SocketShutdown how) + { + BaseSocket.Shutdown(how); + } + + public void Dispose() + { + BaseSocket.Dispose(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs new file mode 100644 index 000000000..b7055f08b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs @@ -0,0 +1,47 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + interface ISocketImpl : IDisposable + { + EndPoint RemoteEndPoint { get; } + EndPoint LocalEndPoint { get; } + bool Connected { get; } + bool IsBound { get; } + + AddressFamily AddressFamily { get; } + SocketType SocketType { get; } + ProtocolType ProtocolType { get; } + + bool Blocking { get; set; } + int Available { get; } + + int Receive(Span buffer); + int Receive(Span buffer, SocketFlags flags); + int Receive(Span buffer, SocketFlags flags, out SocketError socketError); + int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP); + + int Send(ReadOnlySpan buffer); + int Send(ReadOnlySpan buffer, SocketFlags flags); + int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError); + int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP); + + bool Poll(int microSeconds, SelectMode mode); + + ISocketImpl Accept(); + + void Bind(EndPoint localEP); + void Connect(EndPoint remoteEP); + void Listen(int backlog); + + void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue); + void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue); + void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue); + + void Shutdown(SocketShutdown how); + void Disconnect(bool reuseSocket); + void Close(); + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs new file mode 100644 index 000000000..485a7f86b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs @@ -0,0 +1,74 @@ +using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; + +namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy +{ + static class SocketHelpers + { + private static LdnProxy _proxy; + + public static void Select(List readEvents, List writeEvents, List errorEvents, int timeout) + { + var readDefault = readEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + var writeDefault = writeEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + var errorDefault = errorEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList(); + + if (readDefault.Count != 0 || writeDefault.Count != 0 || errorDefault.Count != 0) + { + Socket.Select(readDefault, writeDefault, errorDefault, timeout); + } + + void FilterSockets(List removeFrom, List selectedSockets, Func ldnCheck) + { + removeFrom.RemoveAll(socket => + { + switch (socket) + { + case DefaultSocket dsocket: + return !selectedSockets.Contains(dsocket.BaseSocket); + case LdnProxySocket psocket: + return !ldnCheck(psocket); + default: + throw new NotImplementedException(); + } + }); + }; + + FilterSockets(readEvents, readDefault, (socket) => socket.Readable); + FilterSockets(writeEvents, writeDefault, (socket) => socket.Writable); + FilterSockets(errorEvents, errorDefault, (socket) => socket.Error); + } + + public static void RegisterProxy(LdnProxy proxy) + { + if (_proxy != null) + { + UnregisterProxy(); + } + + _proxy = proxy; + } + + public static void UnregisterProxy() + { + _proxy?.Dispose(); + _proxy = null; + } + + public static ISocketImpl CreateSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId) + { + if (_proxy != null) + { + if (_proxy.Supported(domain, type, protocol)) + { + return new LdnProxySocket(domain, type, protocol, _proxy); + } + } + + return new DefaultSocket(domain, type, protocol, lanInterfaceId); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs index 39af90383..5b2de13f0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs @@ -292,7 +292,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres { string host = MemoryHelper.ReadAsciiString(context.Memory, inputBufferPosition, (int)inputBufferSize); - if (!context.Device.Configuration.EnableInternetAccess) + if (host != "localhost" && !context.Device.Configuration.EnableInternetAccess) { Logger.Info?.Print(LogClass.ServiceSfdnsres, $"Guest network access disabled, DNS Blocked: {host}"); diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs index 8cc761baf..dc33dd6a5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs @@ -1,5 +1,6 @@ using Ryujinx.HLE.HOS.Services.Sockets.Bsd; using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl; +using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy; using Ryujinx.HLE.HOS.Services.Ssl.Types; using System; using System.IO; @@ -116,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService public ResultCode Handshake(string hostName) { StartSslOperation(); - _stream = new SslStream(new NetworkStream(((ManagedSocket)Socket).Socket, false), false, null, null); + _stream = new SslStream(new NetworkStream(((DefaultSocket)((ManagedSocket)Socket).Socket).BaseSocket, false), false, null, null); hostName = RetrieveHostName(hostName); _stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false); EndSslOperation(); diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs index 4c17e7aed..601e85867 100644 --- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs +++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/SurfaceFlinger.cs @@ -10,13 +10,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger { class SurfaceFlinger : IConsumerListener, IDisposable { - private const int TargetFps = 60; - private readonly Switch _device; private readonly Dictionary _layers; @@ -32,6 +31,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger private readonly long _spinTicks; private readonly long _1msTicks; + private VSyncMode _vSyncMode; + private long _targetVSyncInterval; + private int _swapInterval; private int _swapIntervalDelay; @@ -88,7 +90,8 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger } else { - _ticksPerFrame = Stopwatch.Frequency / TargetFps; + _ticksPerFrame = Stopwatch.Frequency / _device.TargetVSyncInterval; + _targetVSyncInterval = _device.TargetVSyncInterval; } } @@ -370,15 +373,20 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger if (acquireStatus == Status.Success) { - // If device vsync is disabled, reflect the change. - if (!_device.EnableDeviceVsync) + if (_device.VSyncMode == VSyncMode.Unbounded) { if (_swapInterval != 0) { UpdateSwapInterval(0); + _vSyncMode = _device.VSyncMode; } } - else if (item.SwapInterval != _swapInterval) + else if (_device.VSyncMode != _vSyncMode) + { + UpdateSwapInterval(_device.VSyncMode == VSyncMode.Unbounded ? 0 : item.SwapInterval); + _vSyncMode = _device.VSyncMode; + } + else if (item.SwapInterval != _swapInterval || _device.TargetVSyncInterval != _targetVSyncInterval) { UpdateSwapInterval(item.SwapInterval); } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index 10561a5a1..e187b2360 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -85,8 +85,8 @@ namespace Ryujinx.HLE.Loaders.Processes } // TODO: LibHac npdm currently doesn't support version field. - string version = ProgramId > 0x0100000000007FFF - ? DisplayVersion + string version = ProgramId > 0x0100000000007FFF + ? DisplayVersion : device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?"; Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]"); diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index a7bb3cd7f..83e7b8810 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* @@ -29,6 +30,7 @@ + diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index d12cb8f77..466352152 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -27,7 +27,11 @@ namespace Ryujinx.HLE public TamperMachine TamperMachine { get; } public IHostUIHandler UIHandler { get; } - public bool EnableDeviceVsync { get; set; } + public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch; + public bool CustomVSyncIntervalEnabled { get; set; } = false; + public int CustomVSyncInterval { get; set; } + + public long TargetVSyncInterval { get; set; } = 60; public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable; @@ -59,12 +63,14 @@ namespace Ryujinx.HLE System.State.SetLanguage(Configuration.SystemLanguage); System.State.SetRegion(Configuration.Region); - EnableDeviceVsync = Configuration.EnableVsync; + VSyncMode = Configuration.VSyncMode; + CustomVSyncInterval = Configuration.CustomVSyncInterval; System.State.DockedMode = Configuration.EnableDockedMode; System.PerformanceState.PerformanceMode = System.State.DockedMode ? PerformanceMode.Boost : PerformanceMode.Default; System.EnablePtc = Configuration.EnablePtc; System.FsIntegrityCheckLevel = Configuration.FsIntegrityCheckLevel; System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode; + UpdateVSyncInterval(); #pragma warning restore IDE0055 } @@ -75,6 +81,34 @@ namespace Ryujinx.HLE Gpu.GPFifo.DispatchCalls(); } + public void IncrementCustomVSyncInterval() + { + CustomVSyncInterval += 1; + UpdateVSyncInterval(); + } + + public void DecrementCustomVSyncInterval() + { + CustomVSyncInterval -= 1; + UpdateVSyncInterval(); + } + + public void UpdateVSyncInterval() + { + switch (VSyncMode) + { + case VSyncMode.Custom: + TargetVSyncInterval = CustomVSyncInterval; + break; + case VSyncMode.Switch: + TargetVSyncInterval = 60; + break; + case VSyncMode.Unbounded: + TargetVSyncInterval = 1; + break; + } + } + public bool LoadCart(string exeFsDir, string romFsFile = null) => Processes.LoadUnpackedNca(exeFsDir, romFsFile); public bool LoadXci(string xciFile, ulong applicationId = 0) => Processes.LoadXci(xciFile, applicationId); public bool LoadNca(string ncaFile) => Processes.LoadNca(ncaFile); diff --git a/src/Ryujinx.HLE/UI/IHostUIHandler.cs b/src/Ryujinx.HLE/UI/IHostUIHandler.cs index 8debfcca0..88af83735 100644 --- a/src/Ryujinx.HLE/UI/IHostUIHandler.cs +++ b/src/Ryujinx.HLE/UI/IHostUIHandler.cs @@ -24,6 +24,18 @@ namespace Ryujinx.HLE.UI /// True when OK is pressed, False otherwise. bool DisplayMessageDialog(ControllerAppletUIArgs args); + /// + /// Displays an Input Dialog box to the user so they can enter the Amiibo's new name + /// + /// Text that the user entered. Set to `null` on internal errors + /// True when OK is pressed, False otherwise. Also returns True on internal errors + bool DisplayCabinetDialog(out string userText); + + /// + /// Displays a Message Dialog box to the user to notify them to scan the Amiibo. + /// + void DisplayCabinetMessageDialog(); + /// /// Tell the UI that we need to transition to another program. /// diff --git a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs index ce52f84d9..8c4854a11 100644 --- a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs +++ b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs @@ -117,8 +117,9 @@ namespace Ryujinx.Headless.SDL2.OpenGL GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) - : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { _glLogLevel = glLogLevel; } diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs index 2f86f3ebf..4e2ad5b58 100644 --- a/src/Ryujinx.Headless.SDL2/Options.cs +++ b/src/Ryujinx.Headless.SDL2/Options.cs @@ -115,8 +115,11 @@ namespace Ryujinx.Headless.SDL2 [Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")] public int FsGlobalAccessLogMode { get; set; } - [Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")] - public bool DisableVSync { get; set; } + [Option("vsync-mode", Required = false, Default = VSyncMode.Switch, HelpText = "Sets the emulated VSync mode (Switch, Unbounded, or Custom).")] + public VSyncMode VSyncMode { get; set; } + + [Option("custom-refresh-rate", Required = false, Default = 90, HelpText = "Sets the custom refresh rate target value (integer).")] + public int CustomVSyncInterval { get; set; } [Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")] public bool DisableShaderCache { get; set; } @@ -225,6 +228,9 @@ namespace Ryujinx.Headless.SDL2 [Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")] public bool IgnoreMissingServices { get; set; } + + [Option("ignore-controller-applet", Required = false, Default = false, HelpText = "Enable ignoring the controller applet when your game loses connection to your controller.")] + public bool IgnoreControllerApplet { get; set; } // Values diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index b6ccb2ac4..ff87a3845 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -444,8 +444,7 @@ namespace Ryujinx.Headless.SDL2 { Logger.AddTarget(new AsyncLogTargetWrapper( new FileLogTarget("file", logFile), - 1000, - AsyncLogTargetOverflowAction.Block + 1000 )); } else @@ -506,8 +505,8 @@ namespace Ryujinx.Headless.SDL2 private static WindowBase CreateWindow(Options options) { return options.GraphicsBackend == GraphicsBackend.Vulkan - ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode) - : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode); + ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet) + : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet); } private static IRenderer CreateRenderer(Options options, WindowBase window) @@ -564,7 +563,7 @@ namespace Ryujinx.Headless.SDL2 window, options.SystemLanguage, options.SystemRegion, - !options.DisableVSync, + options.VSyncMode, !options.DisableDockedMode, !options.DisablePTC, options.EnableInternetAccess, @@ -578,7 +577,11 @@ namespace Ryujinx.Headless.SDL2 options.AudioVolume, options.UseHypervisor ?? true, options.MultiplayerLanInterfaceId, - Common.Configuration.Multiplayer.MultiplayerMode.Disabled); + Common.Configuration.Multiplayer.MultiplayerMode.Disabled, + false, + "", + "", + options.CustomVSyncInterval); return new Switch(configuration); } diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj index 610229544..8fbf9be1e 100644 --- a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -9,6 +9,7 @@ $(DefineConstants);$(ExtraDefineConstants) - true + $(DefaultItemExcludes);._* @@ -17,7 +18,7 @@ - + diff --git a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs index cd7715712..c1dd3805f 100644 --- a/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs +++ b/src/Ryujinx.Headless.SDL2/StatusUpdatedEventArgs.cs @@ -3,7 +3,7 @@ using System; namespace Ryujinx.Headless.SDL2 { class StatusUpdatedEventArgs( - bool vSyncEnabled, + string vSyncMode, string dockedMode, string aspectRatio, string gameStatus, @@ -11,7 +11,7 @@ namespace Ryujinx.Headless.SDL2 string gpuName) : EventArgs { - public bool VSyncEnabled = vSyncEnabled; + public string VSyncMode = vSyncMode; public string DockedMode = dockedMode; public string AspectRatio = aspectRatio; public string GameStatus = gameStatus; diff --git a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs index fb73ca335..b88e0fe83 100644 --- a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs +++ b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs @@ -17,8 +17,9 @@ namespace Ryujinx.Headless.SDL2.Vulkan GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) - : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { _glLogLevel = glLogLevel; } diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs index bd47dfd5d..fbe7cb49c 100644 --- a/src/Ryujinx.Headless.SDL2/WindowBase.cs +++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs @@ -1,4 +1,5 @@ using Humanizer; +using LibHac.Tools.Fs; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Logging; @@ -86,13 +87,15 @@ namespace Ryujinx.Headless.SDL2 private readonly AspectRatio _aspectRatio; private readonly bool _enableMouse; + private readonly bool _ignoreControllerApplet; public WindowBase( InputManager inputManager, GraphicsDebugLevel glLogLevel, AspectRatio aspectRatio, bool enableMouse, - HideCursorMode hideCursorMode) + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) { MouseDriver = new SDL2MouseDriver(hideCursorMode); _inputManager = inputManager; @@ -108,6 +111,7 @@ namespace Ryujinx.Headless.SDL2 _gpuDoneEvent = new ManualResetEvent(false); _aspectRatio = aspectRatio; _enableMouse = enableMouse; + _ignoreControllerApplet = ignoreControllerApplet; HostUITheme = new HeadlessHostUiTheme(); SDL2Driver.Instance.Initialize(); @@ -311,7 +315,7 @@ namespace Ryujinx.Headless.SDL2 } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + Device.VSyncMode.ToString(), dockedMode, Device.Configuration.AspectRatio.ToText(), $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", @@ -482,8 +486,23 @@ namespace Ryujinx.Headless.SDL2 return true; } + public bool DisplayCabinetDialog(out string userText) + { + // SDL2 doesn't support input dialogs + userText = "Ryujinx"; + + return true; + } + + public void DisplayCabinetMessageDialog() + { + SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, "Cabinet Dialog", "Please scan your Amiibo now.", WindowHandle); + } + public bool DisplayMessageDialog(ControllerAppletUIArgs args) { + if (_ignoreControllerApplet) return false; + string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; string message = $"Application requests {playerCount} {"player".ToQuantity(args.PlayerCountMin + args.PlayerCountMax, ShowQuantityAs.None)} with:\n\n" diff --git a/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj b/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj index fa1544c4f..00e0b1af9 100644 --- a/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj +++ b/src/Ryujinx.Horizon.Common/Ryujinx.Horizon.Common.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Horizon.Generators/Ryujinx.Horizon.Generators.csproj b/src/Ryujinx.Horizon.Generators/Ryujinx.Horizon.Generators.csproj index d58803993..416eefc27 100644 --- a/src/Ryujinx.Horizon.Generators/Ryujinx.Horizon.Generators.csproj +++ b/src/Ryujinx.Horizon.Generators/Ryujinx.Horizon.Generators.csproj @@ -3,6 +3,7 @@ netstandard2.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Horizon.Kernel.Generators/Ryujinx.Horizon.Kernel.Generators.csproj b/src/Ryujinx.Horizon.Kernel.Generators/Ryujinx.Horizon.Kernel.Generators.csproj index d58803993..02a8ec2c6 100644 --- a/src/Ryujinx.Horizon.Kernel.Generators/Ryujinx.Horizon.Kernel.Generators.csproj +++ b/src/Ryujinx.Horizon.Kernel.Generators/Ryujinx.Horizon.Kernel.Generators.csproj @@ -3,6 +3,8 @@ netstandard2.0 true + $(DefaultItemExcludes);._* + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj index bf34ddd17..18c639d67 100644 --- a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj +++ b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj b/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj index 1ab79d08a..3d880d5fa 100644 --- a/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj +++ b/src/Ryujinx.Input.SDL2/Ryujinx.Input.SDL2.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs index 0acbaaa19..fd34fe219 100644 --- a/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs +++ b/src/Ryujinx.Input.SDL2/SDL2GamepadDriver.cs @@ -115,7 +115,10 @@ namespace Ryujinx.Input.SDL2 { lock (_lock) { - _gamepadsIds.Insert(joystickDeviceId, id); + if (joystickDeviceId <= _gamepadsIds.FindLastIndex(_ => true)) + _gamepadsIds.Insert(joystickDeviceId, id); + else + _gamepadsIds.Add(id); } OnGamepadConnected?.Invoke(id); diff --git a/src/Ryujinx.Input/Ryujinx.Input.csproj b/src/Ryujinx.Input/Ryujinx.Input.csproj index 59a9eeb61..0974b707a 100644 --- a/src/Ryujinx.Input/Ryujinx.Input.csproj +++ b/src/Ryujinx.Input/Ryujinx.Input.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Memory/Ryujinx.Memory.csproj b/src/Ryujinx.Memory/Ryujinx.Memory.csproj index 8310a3e5c..17745dd61 100644 --- a/src/Ryujinx.Memory/Ryujinx.Memory.csproj +++ b/src/Ryujinx.Memory/Ryujinx.Memory.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Memory/SparseMemoryBlock.cs b/src/Ryujinx.Memory/SparseMemoryBlock.cs new file mode 100644 index 000000000..523685de1 --- /dev/null +++ b/src/Ryujinx.Memory/SparseMemoryBlock.cs @@ -0,0 +1,125 @@ +using Ryujinx.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Ryujinx.Memory +{ + public delegate void PageInitDelegate(Span page); + + public class SparseMemoryBlock : IDisposable + { + private const ulong MapGranularity = 1UL << 17; + + private readonly PageInitDelegate _pageInit; + + private readonly object _lock = new object(); + private readonly ulong _pageSize; + private readonly MemoryBlock _reservedBlock; + private readonly List _mappedBlocks; + private ulong _mappedBlockUsage; + + private readonly ulong[] _mappedPageBitmap; + + public MemoryBlock Block => _reservedBlock; + + public SparseMemoryBlock(ulong size, PageInitDelegate pageInit, MemoryBlock fill) + { + _pageSize = MemoryBlock.GetPageSize(); + _reservedBlock = new MemoryBlock(size, MemoryAllocationFlags.Reserve | MemoryAllocationFlags.ViewCompatible); + _mappedBlocks = new List(); + _pageInit = pageInit; + + int pages = (int)BitUtils.DivRoundUp(size, _pageSize); + int bitmapEntries = BitUtils.DivRoundUp(pages, 64); + _mappedPageBitmap = new ulong[bitmapEntries]; + + if (fill != null) + { + // Fill the block with mappings from the fill block. + + if (fill.Size % _pageSize != 0) + { + throw new ArgumentException("Fill memory block should be page aligned.", nameof(fill)); + } + + int repeats = (int)BitUtils.DivRoundUp(size, fill.Size); + + ulong offset = 0; + for (int i = 0; i < repeats; i++) + { + _reservedBlock.MapView(fill, 0, offset, Math.Min(fill.Size, size - offset)); + offset += fill.Size; + } + } + + // If a fill block isn't provided, the pages that aren't EnsureMapped are unmapped. + // The caller can rely on signal handler to fill empty pages instead. + } + + private void MapPage(ulong pageOffset) + { + // Take a page from the latest mapped block. + MemoryBlock block = _mappedBlocks.LastOrDefault(); + + if (block == null || _mappedBlockUsage == MapGranularity) + { + // Need to map some more memory. + + block = new MemoryBlock(MapGranularity, MemoryAllocationFlags.Mirrorable); + + _mappedBlocks.Add(block); + + _mappedBlockUsage = 0; + } + + _pageInit(block.GetSpan(_mappedBlockUsage, (int)_pageSize)); + _reservedBlock.MapView(block, _mappedBlockUsage, pageOffset, _pageSize); + + _mappedBlockUsage += _pageSize; + } + + public void EnsureMapped(ulong offset) + { + int pageIndex = (int)(offset / _pageSize); + int bitmapIndex = pageIndex >> 6; + + ref ulong entry = ref _mappedPageBitmap[bitmapIndex]; + ulong bit = 1UL << (pageIndex & 63); + + if ((Volatile.Read(ref entry) & bit) == 0) + { + // Not mapped. + + lock (_lock) + { + // Check the bit while locked to make sure that this only happens once. + + ulong lockedEntry = Volatile.Read(ref entry); + + if ((lockedEntry & bit) == 0) + { + MapPage(offset & ~(_pageSize - 1)); + + lockedEntry |= bit; + + Interlocked.Exchange(ref entry, lockedEntry); + } + } + } + } + + public void Dispose() + { + _reservedBlock.Dispose(); + + foreach (MemoryBlock block in _mappedBlocks) + { + block.Dispose(); + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj b/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj index 8e7953045..0811ad850 100644 --- a/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj +++ b/src/Ryujinx.SDL2.Common/Ryujinx.SDL2.Common.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj b/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj index ab89fb5c7..639ceeac2 100644 --- a/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj +++ b/src/Ryujinx.ShaderTools/Ryujinx.ShaderTools.csproj @@ -4,6 +4,7 @@ net8.0 Exe Debug;Release + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj index f05060838..3bb4bf74d 100644 --- a/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj +++ b/src/Ryujinx.Tests.Memory/Ryujinx.Tests.Memory.csproj @@ -3,6 +3,7 @@ net8.0 false + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj b/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj index befacfb22..2f7695356 100644 --- a/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj +++ b/src/Ryujinx.Tests.Unicorn/Ryujinx.Tests.Unicorn.csproj @@ -4,6 +4,7 @@ net8.0 true Debug;Release + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.Tests/Cpu/CpuContext.cs b/src/Ryujinx.Tests/Cpu/CpuContext.cs index 96b4965a2..81e8ba8c9 100644 --- a/src/Ryujinx.Tests/Cpu/CpuContext.cs +++ b/src/Ryujinx.Tests/Cpu/CpuContext.cs @@ -1,3 +1,4 @@ +using ARMeilleure.Common; using ARMeilleure.Memory; using ARMeilleure.State; using ARMeilleure.Translation; @@ -12,7 +13,7 @@ namespace Ryujinx.Tests.Cpu public CpuContext(IMemoryManager memory, bool for64Bit) { - _translator = new Translator(new JitMemoryAllocator(), memory, for64Bit); + _translator = new Translator(new JitMemoryAllocator(), memory, AddressTable.CreateForArm(for64Bit, memory.Type)); memory.UnmapEvent += UnmapHandler; } diff --git a/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs b/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs index 2a4775a31..43c84c193 100644 --- a/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs +++ b/src/Ryujinx.Tests/Cpu/EnvironmentTests.cs @@ -1,3 +1,5 @@ +using ARMeilleure.Common; +using ARMeilleure.Memory; using ARMeilleure.Translation; using NUnit.Framework; using Ryujinx.Cpu.Jit; @@ -17,7 +19,10 @@ namespace Ryujinx.Tests.Cpu private static void EnsureTranslator() { // Create a translator, as one is needed to register the signal handler or emit methods. - _translator ??= new Translator(new JitMemoryAllocator(), new MockMemoryManager(), true); + _translator ??= new Translator( + new JitMemoryAllocator(), + new MockMemoryManager(), + AddressTable.CreateForArm(true, MemoryManagerType.SoftwarePageTable)); } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] diff --git a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs index 6d2ad8fb0..3e5b47423 100644 --- a/src/Ryujinx.Tests/Memory/PartialUnmaps.cs +++ b/src/Ryujinx.Tests/Memory/PartialUnmaps.cs @@ -1,3 +1,5 @@ +using ARMeilleure.Common; +using ARMeilleure.Memory; using ARMeilleure.Signal; using ARMeilleure.Translation; using NUnit.Framework; @@ -53,7 +55,10 @@ namespace Ryujinx.Tests.Memory private static void EnsureTranslator() { // Create a translator, as one is needed to register the signal handler or emit methods. - _translator ??= new Translator(new JitMemoryAllocator(), new MockMemoryManager(), true); + _translator ??= new Translator( + new JitMemoryAllocator(), + new MockMemoryManager(), + AddressTable.CreateForArm(true, MemoryManagerType.SoftwarePageTable)); } [Test] diff --git a/src/Ryujinx.Tests/Ryujinx.Tests.csproj b/src/Ryujinx.Tests/Ryujinx.Tests.csproj index 3be9787a3..0480c206e 100644 --- a/src/Ryujinx.Tests/Ryujinx.Tests.csproj +++ b/src/Ryujinx.Tests/Ryujinx.Tests.csproj @@ -10,6 +10,7 @@ linux Debug;Release $(MSBuildProjectDirectory)\.runsettings + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs index 4c1c1a043..7aa0dccaa 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationData.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs @@ -27,6 +27,8 @@ namespace Ryujinx.UI.App.Common public ulong Id { get; set; } public string Developer { get; set; } = "Unknown"; public string Version { get; set; } = "0"; + public int PlayerCount { get; set; } + public int GameCount { get; set; } public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } public string FileExtension { get; set; } diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 044eccbea..174db51ad 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -12,6 +12,7 @@ using LibHac.Tools.Fs; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Multiplayer; using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.HLE.FileSystem; @@ -27,10 +28,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Reflection; using System.Text; using System.Text.Json; using System.Threading; +using System.Threading.Tasks; using ContentType = LibHac.Ncm.ContentType; using MissingKeyException = LibHac.Common.Keys.MissingKeyException; using Path = System.IO.Path; @@ -41,8 +44,10 @@ namespace Ryujinx.UI.App.Common { public class ApplicationLibrary { + public static string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com"; public Language DesiredLanguage { get; set; } public event EventHandler ApplicationCountUpdated; + public event EventHandler LdnGameDataReceived; public readonly IObservableCache Applications; public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates; @@ -62,6 +67,7 @@ namespace Ryujinx.UI.App.Common private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc); private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel) { @@ -687,7 +693,7 @@ namespace Ryujinx.UI.App.Common (Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0) || (Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI) || (Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA) || - (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) || + (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) || (Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO) ); @@ -719,6 +725,7 @@ namespace Ryujinx.UI.App.Common } } + // Loops through applications list, creating a struct and then firing an event containing the struct for each application foreach (string applicationPath in applicationPaths) { @@ -775,6 +782,46 @@ namespace Ryujinx.UI.App.Common } } + public async Task RefreshLdn() + { + + if (ConfigurationState.Instance.Multiplayer.Mode == MultiplayerMode.LdnRyu) + { + try + { + string ldnWebHost = ConfigurationState.Instance.Multiplayer.LdnServer; + if (string.IsNullOrEmpty(ldnWebHost)) + { + ldnWebHost = DefaultLanPlayWebHost; + } + IEnumerable ldnGameDataArray = Array.Empty(); + using HttpClient httpClient = new HttpClient(); + string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games"); + ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData); + var evt = new LdnGameDataReceivedEventArgs + { + LdnData = ldnGameDataArray + }; + LdnGameDataReceived?.Invoke(null, evt); + } + catch (Exception ex) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.\n{ex.Message}"); + LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs() + { + LdnData = Array.Empty() + }); + } + } + else + { + LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs() + { + LdnData = Array.Empty() + }); + } + } + // Replace the currently stored DLC state for the game with the provided DLC state. public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs) { diff --git a/src/Ryujinx.UI.Common/App/LdnGameData.cs b/src/Ryujinx.UI.Common/App/LdnGameData.cs new file mode 100644 index 000000000..6c784c991 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Ryujinx.UI.App.Common +{ + public struct LdnGameData + { + public string Id { get; set; } + public int PlayerCount { get; set; } + public int MaxPlayerCount { get; set; } + public string GameName { get; set; } + public string TitleId { get; set; } + public string Mode { get; set; } + public string Status { get; set; } + public IEnumerable Players { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs new file mode 100644 index 000000000..7c7454411 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.UI.App.Common +{ + public class LdnGameDataReceivedEventArgs : EventArgs + { + public IEnumerable LdnData { get; set; } + } +} diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs new file mode 100644 index 000000000..ce8edcdb6 --- /dev/null +++ b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Ryujinx.UI.App.Common +{ + [JsonSerializable(typeof(IEnumerable))] + internal partial class LdnGameDataSerializerContext : JsonSerializerContext + { + + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index 77c6346f2..027e1052b 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -1,3 +1,4 @@ +using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Multiplayer; @@ -16,7 +17,7 @@ namespace Ryujinx.UI.Common.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 56; + public const int CurrentVersion = 57; /// /// Version of the configuration file format @@ -191,8 +192,25 @@ namespace Ryujinx.UI.Common.Configuration /// /// Enables or disables Vertical Sync /// + /// Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions) + /// TODO: Remove this when those older versions aren't in use anymore. public bool EnableVsync { get; set; } + /// + /// Current VSync mode; 60 (Switch), unbounded ("Vsync off"), or custom + /// + public VSyncMode VSyncMode { get; set; } + + /// + /// Enables or disables the custom present interval + /// + public bool EnableCustomVSyncInterval { get; set; } + + /// + /// The custom present interval value + /// + public int CustomVSyncInterval { get; set; } + /// /// Enables or disables Shader cache /// @@ -392,6 +410,21 @@ namespace Ryujinx.UI.Common.Configuration /// public string MultiplayerLanInterfaceId { get; set; } + /// + /// Disable P2p Toggle + /// + public bool MultiplayerDisableP2p { get; set; } + + /// + /// Local network passphrase, for private networks. + /// + public string MultiplayerLdnPassphrase { get; set; } + + /// + /// Custom LDN Server + /// + public string LdnServer { get; set; } + /// /// Uses Hypervisor over JIT if available /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs new file mode 100644 index 000000000..a41ea2cd7 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs @@ -0,0 +1,747 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Configuration.UI; +using System; +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Configuration +{ + public partial class ConfigurationState + { + public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) + { + bool configurationFileUpdated = false; + + if (configurationFileFormat.Version is < 0 or > ConfigurationFileFormat.CurrentVersion) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); + + LoadDefault(); + } + + if (configurationFileFormat.Version < 2) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); + + configurationFileFormat.SystemRegion = Region.USA; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 3) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3."); + + configurationFileFormat.SystemTimeZone = "UTC"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 4) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4."); + + configurationFileFormat.MaxAnisotropy = -1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 5) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5."); + + configurationFileFormat.SystemTimeOffset = 0; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 8) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); + + configurationFileFormat.EnablePtc = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 9) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9."); + + configurationFileFormat.ColumnSort = new ColumnSort + { + SortColumnId = 0, + SortAscending = false, + }; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = Key.F1, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 10) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10."); + + configurationFileFormat.AudioBackend = AudioBackend.OpenAl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 11) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11."); + + configurationFileFormat.ResScale = 1; + configurationFileFormat.ResScaleCustom = 1.0f; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 12) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12."); + + configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 13 -> LDN1 + + if (configurationFileFormat.Version < 14) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14."); + + configurationFileFormat.CheckUpdatesOnStart = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 16) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16."); + + configurationFileFormat.EnableShaderCache = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 17) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17."); + + configurationFileFormat.StartFullscreen = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 18) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18."); + + configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9; + + configurationFileUpdated = true; + } + + // configurationFileFormat.Version == 19 -> LDN2 + + if (configurationFileFormat.Version < 20) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20."); + + configurationFileFormat.ShowConfirmExit = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 21) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21."); + + // Initialize network config. + + configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled; + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 22) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); + + configurationFileFormat.HideCursor = HideCursorMode.Never; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 24) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24."); + + configurationFileFormat.InputConfig = new List + { + new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = "0", + PlayerIndex = PlayerIndex.Player1, + ControllerType = ControllerType.ProController, + LeftJoycon = new LeftJoyconCommonConfig + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + }, + RightJoycon = new RightJoyconCommonConfig + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + RightJoyconStick = new JoyconConfigKeyboardStick + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + }, + }, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 25) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 26) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26."); + + configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 27) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); + + configurationFileFormat.EnableMouse = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 28) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = Key.F1, + Screenshot = Key.F8, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 29) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = Key.F1, + Screenshot = Key.F8, + ShowUI = Key.F4, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 30) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.Rumble = new RumbleConfigController + { + EnableRumble = false, + StrongRumble = 1f, + WeakRumble = 1f, + }; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 31) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31."); + + configurationFileFormat.BackendThreading = BackendThreading.Auto; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 32) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = Key.F5, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 33) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = Key.F2, + }; + + configurationFileFormat.AudioVolume = 1; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 34) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34."); + + configurationFileFormat.EnableInternetAccess = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 35) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35."); + + foreach (InputConfig config in configurationFileFormat.InputConfig) + { + if (config is StandardControllerInputConfig controllerConfig) + { + controllerConfig.RangeLeft = 1.0f; + controllerConfig.RangeRight = 1.0f; + } + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 36) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36."); + + configurationFileFormat.LoggingEnableTrace = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 37) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37."); + + configurationFileFormat.ShowConsole = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 38) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); + + configurationFileFormat.BaseStyle = "Dark"; + configurationFileFormat.GameListViewMode = 0; + configurationFileFormat.ShowNames = true; + configurationFileFormat.GridSize = 2; + configurationFileFormat.LanguageCode = "en_US"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 39) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = Key.Unbound, + ResScaleDown = Key.Unbound, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 40) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40."); + + configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 41) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41."); + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, + ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, + VolumeUp = Key.Unbound, + VolumeDown = Key.Unbound, + }; + } + + if (configurationFileFormat.Version < 42) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42."); + + configurationFileFormat.EnableMacroHLE = true; + } + + if (configurationFileFormat.Version < 43) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43."); + + configurationFileFormat.UseHypervisor = true; + } + + if (configurationFileFormat.Version < 44) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44."); + + configurationFileFormat.AntiAliasing = AntiAliasing.None; + configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear; + configurationFileFormat.ScalingFilterLevel = 80; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 45) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45."); + + configurationFileFormat.ShownFileTypes = new ShownFileTypes + { + NSP = true, + PFS0 = true, + XCI = true, + NCA = true, + NRO = true, + NSO = true, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 46) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46."); + + configurationFileFormat.MultiplayerLanInterfaceId = "0"; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 47) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47."); + + configurationFileFormat.WindowStartup = new WindowStartup + { + WindowPositionX = 0, + WindowPositionY = 0, + WindowSizeHeight = 760, + WindowSizeWidth = 1280, + WindowMaximized = false, + }; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 48) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48."); + + configurationFileFormat.EnableColorSpacePassthrough = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 49) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); + + if (OperatingSystem.IsMacOS()) + { + AppDataManager.FixMacOSConfigurationFolders(); + } + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 50) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 50."); + + configurationFileFormat.EnableHardwareAcceleration = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 51) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51."); + + configurationFileFormat.RememberWindowState = true; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 52) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); + + configurationFileFormat.AutoloadDirs = []; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 53) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53."); + + configurationFileFormat.EnableLowPowerPtc = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 54) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 54."); + + configurationFileFormat.DramSize = MemoryConfiguration.MemoryConfiguration4GiB; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 55) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 55."); + + configurationFileFormat.IgnoreApplet = false; + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 56) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 56."); + + configurationFileFormat.ShowTitleBar = !OperatingSystem.IsWindows(); + + configurationFileUpdated = true; + } + + if (configurationFileFormat.Version < 57) + { + Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 57."); + + configurationFileFormat.VSyncMode = VSyncMode.Switch; + configurationFileFormat.EnableCustomVSyncInterval = false; + + configurationFileFormat.Hotkeys = new KeyboardHotkeys + { + ToggleVSyncMode = Key.F1, + Screenshot = configurationFileFormat.Hotkeys.Screenshot, + ShowUI = configurationFileFormat.Hotkeys.ShowUI, + Pause = configurationFileFormat.Hotkeys.Pause, + ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, + ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, + ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, + VolumeUp = configurationFileFormat.Hotkeys.VolumeUp, + VolumeDown = configurationFileFormat.Hotkeys.VolumeDown, + CustomVSyncIntervalIncrement = Key.Unbound, + CustomVSyncIntervalDecrement = Key.Unbound, + }; + + configurationFileFormat.CustomVSyncInterval = 120; + + configurationFileUpdated = true; + } + + Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; + Graphics.ResScale.Value = configurationFileFormat.ResScale; + Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; + Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; + Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; + Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; + Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; + Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; + Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; + Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; + Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; + Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel; + Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug; + Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub; + Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo; + Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn; + Logger.EnableError.Value = configurationFileFormat.LoggingEnableError; + Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace; + Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest; + Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog; + Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses; + Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel; + System.Language.Value = configurationFileFormat.SystemLanguage; + System.Region.Value = configurationFileFormat.SystemRegion; + System.TimeZone.Value = configurationFileFormat.SystemTimeZone; + System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset; + System.EnableDockedMode.Value = configurationFileFormat.DockedMode; + EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; + CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; + ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; + IgnoreApplet.Value = configurationFileFormat.IgnoreApplet; + RememberWindowState.Value = configurationFileFormat.RememberWindowState; + ShowTitleBar.Value = configurationFileFormat.ShowTitleBar; + EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; + HideCursor.Value = configurationFileFormat.HideCursor; + Graphics.VSyncMode.Value = configurationFileFormat.VSyncMode; + Graphics.EnableCustomVSyncInterval.Value = configurationFileFormat.EnableCustomVSyncInterval; + Graphics.CustomVSyncInterval.Value = configurationFileFormat.CustomVSyncInterval; + Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; + Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; + Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; + Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough; + System.EnablePtc.Value = configurationFileFormat.EnablePtc; + System.EnableLowPowerPtc.Value = configurationFileFormat.EnableLowPowerPtc; + System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess; + System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; + System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; + System.AudioBackend.Value = configurationFileFormat.AudioBackend; + System.AudioVolume.Value = configurationFileFormat.AudioVolume; + System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; + System.DramSize.Value = configurationFileFormat.DramSize; + System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; + System.UseHypervisor.Value = configurationFileFormat.UseHypervisor; + UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn; + UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn; + UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; + UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; + UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; + UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; + UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; + UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; + UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn; + UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn; + UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; + UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; + UI.GameDirs.Value = configurationFileFormat.GameDirs; + UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs ?? []; + UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; + UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; + UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; + UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA; + UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO; + UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO; + UI.LanguageCode.Value = configurationFileFormat.LanguageCode; + UI.BaseStyle.Value = configurationFileFormat.BaseStyle; + UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode; + UI.ShowNames.Value = configurationFileFormat.ShowNames; + UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder; + UI.GridSize.Value = configurationFileFormat.GridSize; + UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort; + UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen; + UI.ShowConsole.Value = configurationFileFormat.ShowConsole; + UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth; + UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight; + UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX; + UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY; + UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized; + Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; + Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; + Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; + Hid.InputConfig.Value = configurationFileFormat.InputConfig ?? []; + + Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; + Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; + Multiplayer.DisableP2p.Value = configurationFileFormat.MultiplayerDisableP2p; + Multiplayer.LdnPassphrase.Value = configurationFileFormat.MultiplayerLdnPassphrase; + Multiplayer.LdnServer.Value = configurationFileFormat.LdnServer; + + if (configurationFileUpdated) + { + ToFileFormat().SaveConfig(configurationFilePath); + + Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); + } + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs new file mode 100644 index 000000000..f28ce0348 --- /dev/null +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs @@ -0,0 +1,714 @@ +using ARMeilleure; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.Common.Logging; +using Ryujinx.HLE; +using Ryujinx.UI.Common.Configuration.System; +using Ryujinx.UI.Common.Helper; +using System.Collections.Generic; + +namespace Ryujinx.UI.Common.Configuration +{ + public partial class ConfigurationState + { + /// + /// UI configuration section + /// + public class UISection + { + public class Columns + { + public ReactiveObject FavColumn { get; private set; } + public ReactiveObject IconColumn { get; private set; } + public ReactiveObject AppColumn { get; private set; } + public ReactiveObject DevColumn { get; private set; } + public ReactiveObject VersionColumn { get; private set; } + public ReactiveObject LdnInfoColumn { get; private set; } + public ReactiveObject TimePlayedColumn { get; private set; } + public ReactiveObject LastPlayedColumn { get; private set; } + public ReactiveObject FileExtColumn { get; private set; } + public ReactiveObject FileSizeColumn { get; private set; } + public ReactiveObject PathColumn { get; private set; } + + public Columns() + { + FavColumn = new ReactiveObject(); + IconColumn = new ReactiveObject(); + AppColumn = new ReactiveObject(); + DevColumn = new ReactiveObject(); + VersionColumn = new ReactiveObject(); + LdnInfoColumn = new ReactiveObject(); + TimePlayedColumn = new ReactiveObject(); + LastPlayedColumn = new ReactiveObject(); + FileExtColumn = new ReactiveObject(); + FileSizeColumn = new ReactiveObject(); + PathColumn = new ReactiveObject(); + } + } + + public class ColumnSortSettings + { + public ReactiveObject SortColumnId { get; private set; } + public ReactiveObject SortAscending { get; private set; } + + public ColumnSortSettings() + { + SortColumnId = new ReactiveObject(); + SortAscending = new ReactiveObject(); + } + } + + /// + /// Used to toggle which file types are shown in the UI + /// + public class ShownFileTypeSettings + { + public ReactiveObject NSP { get; private set; } + public ReactiveObject PFS0 { get; private set; } + public ReactiveObject XCI { get; private set; } + public ReactiveObject NCA { get; private set; } + public ReactiveObject NRO { get; private set; } + public ReactiveObject NSO { get; private set; } + + public ShownFileTypeSettings() + { + NSP = new ReactiveObject(); + PFS0 = new ReactiveObject(); + XCI = new ReactiveObject(); + NCA = new ReactiveObject(); + NRO = new ReactiveObject(); + NSO = new ReactiveObject(); + } + } + + // + /// Determines main window start-up position, size and state + /// + public class WindowStartupSettings + { + public ReactiveObject WindowSizeWidth { get; private set; } + public ReactiveObject WindowSizeHeight { get; private set; } + public ReactiveObject WindowPositionX { get; private set; } + public ReactiveObject WindowPositionY { get; private set; } + public ReactiveObject WindowMaximized { get; private set; } + + public WindowStartupSettings() + { + WindowSizeWidth = new ReactiveObject(); + WindowSizeHeight = new ReactiveObject(); + WindowPositionX = new ReactiveObject(); + WindowPositionY = new ReactiveObject(); + WindowMaximized = new ReactiveObject(); + } + } + + /// + /// Used to toggle columns in the GUI + /// + public Columns GuiColumns { get; private set; } + + /// + /// Used to configure column sort settings in the GUI + /// + public ColumnSortSettings ColumnSort { get; private set; } + + /// + /// A list of directories containing games to be used to load games into the games list + /// + public ReactiveObject> GameDirs { get; private set; } + + /// + /// A list of directories containing DLC/updates the user wants to autoload during library refreshes + /// + public ReactiveObject> AutoloadDirs { get; private set; } + + /// + /// A list of file types to be hidden in the games List + /// + public ShownFileTypeSettings ShownFileTypes { get; private set; } + + /// + /// Determines main window start-up position, size and state + /// + public WindowStartupSettings WindowStartup { get; private set; } + + /// + /// Language Code for the UI + /// + public ReactiveObject LanguageCode { get; private set; } + + /// + /// Selects the base style + /// + public ReactiveObject BaseStyle { get; private set; } + + /// + /// Start games in fullscreen mode + /// + public ReactiveObject StartFullscreen { get; private set; } + + /// + /// Hide / Show Console Window + /// + public ReactiveObject ShowConsole { get; private set; } + + /// + /// View Mode of the Game list + /// + public ReactiveObject GameListViewMode { get; private set; } + + /// + /// Show application name in Grid Mode + /// + public ReactiveObject ShowNames { get; private set; } + + /// + /// Sets App Icon Size in Grid Mode + /// + public ReactiveObject GridSize { get; private set; } + + /// + /// Sorts Apps in Grid Mode + /// + public ReactiveObject ApplicationSort { get; private set; } + + /// + /// Sets if Grid is ordered in Ascending Order + /// + public ReactiveObject IsAscendingOrder { get; private set; } + + public UISection() + { + GuiColumns = new Columns(); + ColumnSort = new ColumnSortSettings(); + GameDirs = new ReactiveObject>(); + AutoloadDirs = new ReactiveObject>(); + ShownFileTypes = new ShownFileTypeSettings(); + WindowStartup = new WindowStartupSettings(); + BaseStyle = new ReactiveObject(); + StartFullscreen = new ReactiveObject(); + GameListViewMode = new ReactiveObject(); + ShowNames = new ReactiveObject(); + GridSize = new ReactiveObject(); + ApplicationSort = new ReactiveObject(); + IsAscendingOrder = new ReactiveObject(); + LanguageCode = new ReactiveObject(); + ShowConsole = new ReactiveObject(); + ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue); + } + } + + /// + /// Logger configuration section + /// + public class LoggerSection + { + /// + /// Enables printing debug log messages + /// + public ReactiveObject EnableDebug { get; private set; } + + /// + /// Enables printing stub log messages + /// + public ReactiveObject EnableStub { get; private set; } + + /// + /// Enables printing info log messages + /// + public ReactiveObject EnableInfo { get; private set; } + + /// + /// Enables printing warning log messages + /// + public ReactiveObject EnableWarn { get; private set; } + + /// + /// Enables printing error log messages + /// + public ReactiveObject EnableError { get; private set; } + + /// + /// Enables printing trace log messages + /// + public ReactiveObject EnableTrace { get; private set; } + + /// + /// Enables printing guest log messages + /// + public ReactiveObject EnableGuest { get; private set; } + + /// + /// Enables printing FS access log messages + /// + public ReactiveObject EnableFsAccessLog { get; private set; } + + /// + /// Controls which log messages are written to the log targets + /// + public ReactiveObject FilteredClasses { get; private set; } + + /// + /// Enables or disables logging to a file on disk + /// + public ReactiveObject EnableFileLog { get; private set; } + + /// + /// Controls which OpenGL log messages are recorded in the log + /// + public ReactiveObject GraphicsDebugLevel { get; private set; } + + public LoggerSection() + { + EnableDebug = new ReactiveObject(); + EnableDebug.LogChangesToValue(nameof(EnableDebug)); + EnableStub = new ReactiveObject(); + EnableInfo = new ReactiveObject(); + EnableWarn = new ReactiveObject(); + EnableError = new ReactiveObject(); + EnableTrace = new ReactiveObject(); + EnableGuest = new ReactiveObject(); + EnableFsAccessLog = new ReactiveObject(); + FilteredClasses = new ReactiveObject(); + EnableFileLog = new ReactiveObject(); + EnableFileLog.LogChangesToValue(nameof(EnableFileLog)); + GraphicsDebugLevel = new ReactiveObject(); + } + } + + /// + /// System configuration section + /// + public class SystemSection + { + /// + /// Change System Language + /// + public ReactiveObject Language { get; private set; } + + /// + /// Change System Region + /// + public ReactiveObject Region { get; private set; } + + /// + /// Change System TimeZone + /// + public ReactiveObject TimeZone { get; private set; } + + /// + /// System Time Offset in Seconds + /// + public ReactiveObject SystemTimeOffset { get; private set; } + + /// + /// Enables or disables Docked Mode + /// + public ReactiveObject EnableDockedMode { get; private set; } + + /// + /// Enables or disables persistent profiled translation cache + /// + public ReactiveObject EnablePtc { get; private set; } + + /// + /// Enables or disables low-power persistent profiled translation cache loading + /// + public ReactiveObject EnableLowPowerPtc { get; private set; } + + /// + /// Enables or disables guest Internet access + /// + public ReactiveObject EnableInternetAccess { get; private set; } + + /// + /// Enables integrity checks on Game content files + /// + public ReactiveObject EnableFsIntegrityChecks { get; private set; } + + /// + /// Enables FS access log output to the console. Possible modes are 0-3 + /// + public ReactiveObject FsGlobalAccessLogMode { get; private set; } + + /// + /// The selected audio backend + /// + public ReactiveObject AudioBackend { get; private set; } + + /// + /// The audio backend volume + /// + public ReactiveObject AudioVolume { get; private set; } + + /// + /// The selected memory manager mode + /// + public ReactiveObject MemoryManagerMode { get; private set; } + + /// + /// Defines the amount of RAM available on the emulated system, and how it is distributed + /// + public ReactiveObject DramSize { get; private set; } + + /// + /// Enable or disable ignoring missing services + /// + public ReactiveObject IgnoreMissingServices { get; private set; } + + /// + /// Uses Hypervisor over JIT if available + /// + public ReactiveObject UseHypervisor { get; private set; } + + public SystemSection() + { + Language = new ReactiveObject(); + Language.LogChangesToValue(nameof(Language)); + Region = new ReactiveObject(); + Region.LogChangesToValue(nameof(Region)); + TimeZone = new ReactiveObject(); + TimeZone.LogChangesToValue(nameof(TimeZone)); + SystemTimeOffset = new ReactiveObject(); + SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset)); + EnableDockedMode = new ReactiveObject(); + EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode)); + EnablePtc = new ReactiveObject(); + EnablePtc.LogChangesToValue(nameof(EnablePtc)); + EnableLowPowerPtc = new ReactiveObject(); + EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc)); + EnableLowPowerPtc.Event += (_, evnt) + => Optimizations.LowPower = evnt.NewValue; + EnableInternetAccess = new ReactiveObject(); + EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess)); + EnableFsIntegrityChecks = new ReactiveObject(); + EnableFsIntegrityChecks.LogChangesToValue(nameof(EnableFsIntegrityChecks)); + FsGlobalAccessLogMode = new ReactiveObject(); + FsGlobalAccessLogMode.LogChangesToValue(nameof(FsGlobalAccessLogMode)); + AudioBackend = new ReactiveObject(); + AudioBackend.LogChangesToValue(nameof(AudioBackend)); + MemoryManagerMode = new ReactiveObject(); + MemoryManagerMode.LogChangesToValue(nameof(MemoryManagerMode)); + DramSize = new ReactiveObject(); + DramSize.LogChangesToValue(nameof(DramSize)); + IgnoreMissingServices = new ReactiveObject(); + IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices)); + AudioVolume = new ReactiveObject(); + AudioVolume.LogChangesToValue(nameof(AudioVolume)); + UseHypervisor = new ReactiveObject(); + UseHypervisor.LogChangesToValue(nameof(UseHypervisor)); + } + } + + /// + /// Hid configuration section + /// + public class HidSection + { + /// + /// Enable or disable keyboard support (Independent from controllers binding) + /// + public ReactiveObject EnableKeyboard { get; private set; } + + /// + /// Enable or disable mouse support (Independent from controllers binding) + /// + public ReactiveObject EnableMouse { get; private set; } + + /// + /// Hotkey Keyboard Bindings + /// + public ReactiveObject Hotkeys { get; private set; } + + /// + /// Input device configuration. + /// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed. + /// TODO: Implement a ReactiveList class. + /// + public ReactiveObject> InputConfig { get; private set; } + + public HidSection() + { + EnableKeyboard = new ReactiveObject(); + EnableMouse = new ReactiveObject(); + Hotkeys = new ReactiveObject(); + InputConfig = new ReactiveObject>(); + } + } + + /// + /// Graphics configuration section + /// + public class GraphicsSection + { + /// + /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. + /// + public ReactiveObject BackendThreading { get; private set; } + + /// + /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. + /// + public ReactiveObject MaxAnisotropy { get; private set; } + + /// + /// Aspect Ratio applied to the renderer window. + /// + public ReactiveObject AspectRatio { get; private set; } + + /// + /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. + /// + public ReactiveObject ResScale { get; private set; } + + /// + /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. + /// + public ReactiveObject ResScaleCustom { get; private set; } + + /// + /// Dumps shaders in this local directory + /// + public ReactiveObject ShadersDumpPath { get; private set; } + + /// + /// Toggles the present interval mode. Options are Switch (60Hz), Unbounded (previously Vsync off), and Custom, if enabled. + /// + public ReactiveObject VSyncMode { get; private set; } + + /// + /// Enables or disables the custom present interval mode. + /// + public ReactiveObject EnableCustomVSyncInterval { get; private set; } + + /// + /// Changes the custom present interval. + /// + public ReactiveObject CustomVSyncInterval { get; private set; } + + /// + /// Enables or disables Shader cache + /// + public ReactiveObject EnableShaderCache { get; private set; } + + /// + /// Enables or disables texture recompression + /// + public ReactiveObject EnableTextureRecompression { get; private set; } + + /// + /// Enables or disables Macro high-level emulation + /// + public ReactiveObject EnableMacroHLE { get; private set; } + + /// + /// Enables or disables color space passthrough, if available. + /// + public ReactiveObject EnableColorSpacePassthrough { get; private set; } + + /// + /// Graphics backend + /// + public ReactiveObject GraphicsBackend { get; private set; } + + /// + /// Applies anti-aliasing to the renderer. + /// + public ReactiveObject AntiAliasing { get; private set; } + + /// + /// Sets the framebuffer upscaling type. + /// + public ReactiveObject ScalingFilter { get; private set; } + + /// + /// Sets the framebuffer upscaling level. + /// + public ReactiveObject ScalingFilterLevel { get; private set; } + + /// + /// Preferred GPU + /// + public ReactiveObject PreferredGpu { get; private set; } + + public GraphicsSection() + { + BackendThreading = new ReactiveObject(); + BackendThreading.LogChangesToValue(nameof(BackendThreading)); + ResScale = new ReactiveObject(); + ResScale.LogChangesToValue(nameof(ResScale)); + ResScaleCustom = new ReactiveObject(); + ResScaleCustom.LogChangesToValue(nameof(ResScaleCustom)); + MaxAnisotropy = new ReactiveObject(); + MaxAnisotropy.LogChangesToValue(nameof(MaxAnisotropy)); + AspectRatio = new ReactiveObject(); + AspectRatio.LogChangesToValue(nameof(AspectRatio)); + ShadersDumpPath = new ReactiveObject(); + VSyncMode = new ReactiveObject(); + VSyncMode.LogChangesToValue(nameof(VSyncMode)); + EnableCustomVSyncInterval = new ReactiveObject(); + EnableCustomVSyncInterval.LogChangesToValue(nameof(EnableCustomVSyncInterval)); + CustomVSyncInterval = new ReactiveObject(); + CustomVSyncInterval.LogChangesToValue(nameof(CustomVSyncInterval)); + EnableShaderCache = new ReactiveObject(); + EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache)); + EnableTextureRecompression = new ReactiveObject(); + EnableTextureRecompression.LogChangesToValue(nameof(EnableTextureRecompression)); + GraphicsBackend = new ReactiveObject(); + GraphicsBackend.LogChangesToValue(nameof(GraphicsBackend)); + PreferredGpu = new ReactiveObject(); + PreferredGpu.LogChangesToValue(nameof(PreferredGpu)); + EnableMacroHLE = new ReactiveObject(); + EnableMacroHLE.LogChangesToValue(nameof(EnableMacroHLE)); + EnableColorSpacePassthrough = new ReactiveObject(); + EnableColorSpacePassthrough.LogChangesToValue(nameof(EnableColorSpacePassthrough)); + AntiAliasing = new ReactiveObject(); + AntiAliasing.LogChangesToValue(nameof(AntiAliasing)); + ScalingFilter = new ReactiveObject(); + ScalingFilter.LogChangesToValue(nameof(ScalingFilter)); + ScalingFilterLevel = new ReactiveObject(); + ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel)); + } + } + + /// + /// Multiplayer configuration section + /// + public class MultiplayerSection + { + /// + /// GUID for the network interface used by LAN (or 0 for default) + /// + public ReactiveObject LanInterfaceId { get; private set; } + + /// + /// Multiplayer Mode + /// + public ReactiveObject Mode { get; private set; } + + /// + /// Disable P2P + /// + public ReactiveObject DisableP2p { get; private set; } + + /// + /// LDN PassPhrase + /// + public ReactiveObject LdnPassphrase { get; private set; } + + /// + /// LDN Server + /// + public ReactiveObject LdnServer { get; private set; } + + public MultiplayerSection() + { + LanInterfaceId = new ReactiveObject(); + Mode = new ReactiveObject(); + Mode.LogChangesToValue(nameof(MultiplayerMode)); + DisableP2p = new ReactiveObject(); + DisableP2p.LogChangesToValue(nameof(DisableP2p)); + LdnPassphrase = new ReactiveObject(); + LdnPassphrase.LogChangesToValue(nameof(LdnPassphrase)); + LdnServer = new ReactiveObject(); + LdnServer.LogChangesToValue(nameof(LdnServer)); + } + } + + /// + /// The default configuration instance + /// + public static ConfigurationState Instance { get; private set; } + + /// + /// The UI section + /// + public UISection UI { get; private set; } + + /// + /// The Logger section + /// + public LoggerSection Logger { get; private set; } + + /// + /// The System section + /// + public SystemSection System { get; private set; } + + /// + /// The Graphics section + /// + public GraphicsSection Graphics { get; private set; } + + /// + /// The Hid section + /// + public HidSection Hid { get; private set; } + + /// + /// The Multiplayer section + /// + public MultiplayerSection Multiplayer { get; private set; } + + /// + /// Enables or disables Discord Rich Presence + /// + public ReactiveObject EnableDiscordIntegration { get; private set; } + + /// + /// Checks for updates when Ryujinx starts when enabled + /// + public ReactiveObject CheckUpdatesOnStart { get; private set; } + + /// + /// Show "Confirm Exit" Dialog + /// + public ReactiveObject ShowConfirmExit { get; private set; } + + /// + /// Ignore Applet + /// + public ReactiveObject IgnoreApplet { get; private set; } + + /// + /// Enables or disables save window size, position and state on close. + /// + public ReactiveObject RememberWindowState { get; private set; } + + /// + /// Enables or disables the redesigned title bar + /// + public ReactiveObject ShowTitleBar { get; private set; } + + /// + /// Enables hardware-accelerated rendering for Avalonia + /// + public ReactiveObject EnableHardwareAcceleration { get; private set; } + + /// + /// Hide Cursor on Idle + /// + public ReactiveObject HideCursor { get; private set; } + + private ConfigurationState() + { + UI = new UISection(); + Logger = new LoggerSection(); + System = new SystemSection(); + Graphics = new GraphicsSection(); + Hid = new HidSection(); + Multiplayer = new MultiplayerSection(); + EnableDiscordIntegration = new ReactiveObject(); + CheckUpdatesOnStart = new ReactiveObject(); + ShowConfirmExit = new ReactiveObject(); + IgnoreApplet = new ReactiveObject(); + IgnoreApplet.LogChangesToValue(nameof(IgnoreApplet)); + RememberWindowState = new ReactiveObject(); + ShowTitleBar = new ReactiveObject(); + EnableHardwareAcceleration = new ReactiveObject(); + HideCursor = new ReactiveObject(); + } + } +} diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 50b3569a1..badb047df 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -1,5 +1,4 @@ using ARMeilleure; -using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid.Controller; @@ -10,668 +9,22 @@ using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE; using Ryujinx.UI.Common.Configuration.System; using Ryujinx.UI.Common.Configuration.UI; -using Ryujinx.UI.Common.Helper; using System; using System.Collections.Generic; -using System.Globalization; -using System.Text.Json.Nodes; namespace Ryujinx.UI.Common.Configuration { - public class ConfigurationState + public partial class ConfigurationState { - /// - /// UI configuration section - /// - public class UISection + public static void Initialize() { - public class Columns + if (Instance != null) { - public ReactiveObject FavColumn { get; private set; } - public ReactiveObject IconColumn { get; private set; } - public ReactiveObject AppColumn { get; private set; } - public ReactiveObject DevColumn { get; private set; } - public ReactiveObject VersionColumn { get; private set; } - public ReactiveObject TimePlayedColumn { get; private set; } - public ReactiveObject LastPlayedColumn { get; private set; } - public ReactiveObject FileExtColumn { get; private set; } - public ReactiveObject FileSizeColumn { get; private set; } - public ReactiveObject PathColumn { get; private set; } - - public Columns() - { - FavColumn = new ReactiveObject(); - IconColumn = new ReactiveObject(); - AppColumn = new ReactiveObject(); - DevColumn = new ReactiveObject(); - VersionColumn = new ReactiveObject(); - TimePlayedColumn = new ReactiveObject(); - LastPlayedColumn = new ReactiveObject(); - FileExtColumn = new ReactiveObject(); - FileSizeColumn = new ReactiveObject(); - PathColumn = new ReactiveObject(); + throw new InvalidOperationException("Configuration is already initialized"); } - } - public class ColumnSortSettings - { - public ReactiveObject SortColumnId { get; private set; } - public ReactiveObject SortAscending { get; private set; } - - public ColumnSortSettings() - { - SortColumnId = new ReactiveObject(); - SortAscending = new ReactiveObject(); + Instance = new ConfigurationState(); } - } - - /// - /// Used to toggle which file types are shown in the UI - /// - public class ShownFileTypeSettings - { - public ReactiveObject NSP { get; private set; } - public ReactiveObject PFS0 { get; private set; } - public ReactiveObject XCI { get; private set; } - public ReactiveObject NCA { get; private set; } - public ReactiveObject NRO { get; private set; } - public ReactiveObject NSO { get; private set; } - - public ShownFileTypeSettings() - { - NSP = new ReactiveObject(); - PFS0 = new ReactiveObject(); - XCI = new ReactiveObject(); - NCA = new ReactiveObject(); - NRO = new ReactiveObject(); - NSO = new ReactiveObject(); - } - } - - // - /// Determines main window start-up position, size and state - /// - public class WindowStartupSettings - { - public ReactiveObject WindowSizeWidth { get; private set; } - public ReactiveObject WindowSizeHeight { get; private set; } - public ReactiveObject WindowPositionX { get; private set; } - public ReactiveObject WindowPositionY { get; private set; } - public ReactiveObject WindowMaximized { get; private set; } - - public WindowStartupSettings() - { - WindowSizeWidth = new ReactiveObject(); - WindowSizeHeight = new ReactiveObject(); - WindowPositionX = new ReactiveObject(); - WindowPositionY = new ReactiveObject(); - WindowMaximized = new ReactiveObject(); - } - } - - /// - /// Used to toggle columns in the GUI - /// - public Columns GuiColumns { get; private set; } - - /// - /// Used to configure column sort settings in the GUI - /// - public ColumnSortSettings ColumnSort { get; private set; } - - /// - /// A list of directories containing games to be used to load games into the games list - /// - public ReactiveObject> GameDirs { get; private set; } - - /// - /// A list of directories containing DLC/updates the user wants to autoload during library refreshes - /// - public ReactiveObject> AutoloadDirs { get; private set; } - - /// - /// A list of file types to be hidden in the games List - /// - public ShownFileTypeSettings ShownFileTypes { get; private set; } - - /// - /// Determines main window start-up position, size and state - /// - public WindowStartupSettings WindowStartup { get; private set; } - - /// - /// Language Code for the UI - /// - public ReactiveObject LanguageCode { get; private set; } - - /// - /// Selects the base style - /// - public ReactiveObject BaseStyle { get; private set; } - - /// - /// Start games in fullscreen mode - /// - public ReactiveObject StartFullscreen { get; private set; } - - /// - /// Hide / Show Console Window - /// - public ReactiveObject ShowConsole { get; private set; } - - /// - /// View Mode of the Game list - /// - public ReactiveObject GameListViewMode { get; private set; } - - /// - /// Show application name in Grid Mode - /// - public ReactiveObject ShowNames { get; private set; } - - /// - /// Sets App Icon Size in Grid Mode - /// - public ReactiveObject GridSize { get; private set; } - - /// - /// Sorts Apps in Grid Mode - /// - public ReactiveObject ApplicationSort { get; private set; } - - /// - /// Sets if Grid is ordered in Ascending Order - /// - public ReactiveObject IsAscendingOrder { get; private set; } - - public UISection() - { - GuiColumns = new Columns(); - ColumnSort = new ColumnSortSettings(); - GameDirs = new ReactiveObject>(); - AutoloadDirs = new ReactiveObject>(); - ShownFileTypes = new ShownFileTypeSettings(); - WindowStartup = new WindowStartupSettings(); - BaseStyle = new ReactiveObject(); - StartFullscreen = new ReactiveObject(); - GameListViewMode = new ReactiveObject(); - ShowNames = new ReactiveObject(); - GridSize = new ReactiveObject(); - ApplicationSort = new ReactiveObject(); - IsAscendingOrder = new ReactiveObject(); - LanguageCode = new ReactiveObject(); - ShowConsole = new ReactiveObject(); - ShowConsole.Event += static (s, e) => { ConsoleHelper.SetConsoleWindowState(e.NewValue); }; - } - } - - /// - /// Logger configuration section - /// - public class LoggerSection - { - /// - /// Enables printing debug log messages - /// - public ReactiveObject EnableDebug { get; private set; } - - /// - /// Enables printing stub log messages - /// - public ReactiveObject EnableStub { get; private set; } - - /// - /// Enables printing info log messages - /// - public ReactiveObject EnableInfo { get; private set; } - - /// - /// Enables printing warning log messages - /// - public ReactiveObject EnableWarn { get; private set; } - - /// - /// Enables printing error log messages - /// - public ReactiveObject EnableError { get; private set; } - - /// - /// Enables printing trace log messages - /// - public ReactiveObject EnableTrace { get; private set; } - - /// - /// Enables printing guest log messages - /// - public ReactiveObject EnableGuest { get; private set; } - - /// - /// Enables printing FS access log messages - /// - public ReactiveObject EnableFsAccessLog { get; private set; } - - /// - /// Controls which log messages are written to the log targets - /// - public ReactiveObject FilteredClasses { get; private set; } - - /// - /// Enables or disables logging to a file on disk - /// - public ReactiveObject EnableFileLog { get; private set; } - - /// - /// Controls which OpenGL log messages are recorded in the log - /// - public ReactiveObject GraphicsDebugLevel { get; private set; } - - public LoggerSection() - { - EnableDebug = new ReactiveObject(); - EnableStub = new ReactiveObject(); - EnableInfo = new ReactiveObject(); - EnableWarn = new ReactiveObject(); - EnableError = new ReactiveObject(); - EnableTrace = new ReactiveObject(); - EnableGuest = new ReactiveObject(); - EnableFsAccessLog = new ReactiveObject(); - FilteredClasses = new ReactiveObject(); - EnableFileLog = new ReactiveObject(); - EnableFileLog.Event += static (sender, e) => LogValueChange(e, nameof(EnableFileLog)); - GraphicsDebugLevel = new ReactiveObject(); - } - } - - /// - /// System configuration section - /// - public class SystemSection - { - /// - /// Change System Language - /// - public ReactiveObject Language { get; private set; } - - /// - /// Change System Region - /// - public ReactiveObject Region { get; private set; } - - /// - /// Change System TimeZone - /// - public ReactiveObject TimeZone { get; private set; } - - /// - /// System Time Offset in Seconds - /// - public ReactiveObject SystemTimeOffset { get; private set; } - - /// - /// Enables or disables Docked Mode - /// - public ReactiveObject EnableDockedMode { get; private set; } - - /// - /// Enables or disables persistent profiled translation cache - /// - public ReactiveObject EnablePtc { get; private set; } - - /// - /// Enables or disables low-power persistent profiled translation cache loading - /// - public ReactiveObject EnableLowPowerPtc { get; private set; } - - /// - /// Enables or disables guest Internet access - /// - public ReactiveObject EnableInternetAccess { get; private set; } - - /// - /// Enables integrity checks on Game content files - /// - public ReactiveObject EnableFsIntegrityChecks { get; private set; } - - /// - /// Enables FS access log output to the console. Possible modes are 0-3 - /// - public ReactiveObject FsGlobalAccessLogMode { get; private set; } - - /// - /// The selected audio backend - /// - public ReactiveObject AudioBackend { get; private set; } - - /// - /// The audio backend volume - /// - public ReactiveObject AudioVolume { get; private set; } - - /// - /// The selected memory manager mode - /// - public ReactiveObject MemoryManagerMode { get; private set; } - - /// - /// Defines the amount of RAM available on the emulated system, and how it is distributed - /// - public ReactiveObject DramSize { get; private set; } - - /// - /// Enable or disable ignoring missing services - /// - public ReactiveObject IgnoreMissingServices { get; private set; } - - /// - /// Uses Hypervisor over JIT if available - /// - public ReactiveObject UseHypervisor { get; private set; } - - public SystemSection() - { - Language = new ReactiveObject(); - Region = new ReactiveObject(); - TimeZone = new ReactiveObject(); - SystemTimeOffset = new ReactiveObject(); - EnableDockedMode = new ReactiveObject(); - EnableDockedMode.Event += static (sender, e) => LogValueChange(e, nameof(EnableDockedMode)); - EnablePtc = new ReactiveObject(); - EnablePtc.Event += static (sender, e) => LogValueChange(e, nameof(EnablePtc)); - EnableLowPowerPtc = new ReactiveObject(); - EnableLowPowerPtc.Event += static (sender, e) => LogValueChange(e, nameof(EnableLowPowerPtc)); - EnableInternetAccess = new ReactiveObject(); - EnableInternetAccess.Event += static (sender, e) => LogValueChange(e, nameof(EnableInternetAccess)); - EnableFsIntegrityChecks = new ReactiveObject(); - EnableFsIntegrityChecks.Event += static (sender, e) => LogValueChange(e, nameof(EnableFsIntegrityChecks)); - FsGlobalAccessLogMode = new ReactiveObject(); - FsGlobalAccessLogMode.Event += static (sender, e) => LogValueChange(e, nameof(FsGlobalAccessLogMode)); - AudioBackend = new ReactiveObject(); - AudioBackend.Event += static (sender, e) => LogValueChange(e, nameof(AudioBackend)); - MemoryManagerMode = new ReactiveObject(); - MemoryManagerMode.Event += static (sender, e) => LogValueChange(e, nameof(MemoryManagerMode)); - DramSize = new ReactiveObject(); - DramSize.Event += static (sender, e) => LogValueChange(e, nameof(DramSize)); - IgnoreMissingServices = new ReactiveObject(); - IgnoreMissingServices.Event += static (sender, e) => LogValueChange(e, nameof(IgnoreMissingServices)); - AudioVolume = new ReactiveObject(); - AudioVolume.Event += static (sender, e) => LogValueChange(e, nameof(AudioVolume)); - UseHypervisor = new ReactiveObject(); - UseHypervisor.Event += static (sender, e) => LogValueChange(e, nameof(UseHypervisor)); - } - } - - /// - /// Hid configuration section - /// - public class HidSection - { - /// - /// Enable or disable keyboard support (Independent from controllers binding) - /// - public ReactiveObject EnableKeyboard { get; private set; } - - /// - /// Enable or disable mouse support (Independent from controllers binding) - /// - public ReactiveObject EnableMouse { get; private set; } - - /// - /// Hotkey Keyboard Bindings - /// - public ReactiveObject Hotkeys { get; private set; } - - /// - /// Input device configuration. - /// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed. - /// TODO: Implement a ReactiveList class. - /// - public ReactiveObject> InputConfig { get; private set; } - - public HidSection() - { - EnableKeyboard = new ReactiveObject(); - EnableMouse = new ReactiveObject(); - Hotkeys = new ReactiveObject(); - InputConfig = new ReactiveObject>(); - } - } - - /// - /// Graphics configuration section - /// - public class GraphicsSection - { - /// - /// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime. - /// - public ReactiveObject BackendThreading { get; private set; } - - /// - /// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide. - /// - public ReactiveObject MaxAnisotropy { get; private set; } - - /// - /// Aspect Ratio applied to the renderer window. - /// - public ReactiveObject AspectRatio { get; private set; } - - /// - /// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead. - /// - public ReactiveObject ResScale { get; private set; } - - /// - /// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1. - /// - public ReactiveObject ResScaleCustom { get; private set; } - - /// - /// Dumps shaders in this local directory - /// - public ReactiveObject ShadersDumpPath { get; private set; } - - /// - /// Enables or disables Vertical Sync - /// - public ReactiveObject EnableVsync { get; private set; } - - /// - /// Enables or disables Shader cache - /// - public ReactiveObject EnableShaderCache { get; private set; } - - /// - /// Enables or disables texture recompression - /// - public ReactiveObject EnableTextureRecompression { get; private set; } - - /// - /// Enables or disables Macro high-level emulation - /// - public ReactiveObject EnableMacroHLE { get; private set; } - - /// - /// Enables or disables color space passthrough, if available. - /// - public ReactiveObject EnableColorSpacePassthrough { get; private set; } - - /// - /// Graphics backend - /// - public ReactiveObject GraphicsBackend { get; private set; } - - /// - /// Applies anti-aliasing to the renderer. - /// - public ReactiveObject AntiAliasing { get; private set; } - - /// - /// Sets the framebuffer upscaling type. - /// - public ReactiveObject ScalingFilter { get; private set; } - - /// - /// Sets the framebuffer upscaling level. - /// - public ReactiveObject ScalingFilterLevel { get; private set; } - - /// - /// Preferred GPU - /// - public ReactiveObject PreferredGpu { get; private set; } - - public GraphicsSection() - { - BackendThreading = new ReactiveObject(); - BackendThreading.Event += static (_, e) => LogValueChange(e, nameof(BackendThreading)); - ResScale = new ReactiveObject(); - ResScale.Event += static (_, e) => LogValueChange(e, nameof(ResScale)); - ResScaleCustom = new ReactiveObject(); - ResScaleCustom.Event += static (_, e) => LogValueChange(e, nameof(ResScaleCustom)); - MaxAnisotropy = new ReactiveObject(); - MaxAnisotropy.Event += static (_, e) => LogValueChange(e, nameof(MaxAnisotropy)); - AspectRatio = new ReactiveObject(); - AspectRatio.Event += static (_, e) => LogValueChange(e, nameof(AspectRatio)); - ShadersDumpPath = new ReactiveObject(); - EnableVsync = new ReactiveObject(); - EnableVsync.Event += static (_, e) => LogValueChange(e, nameof(EnableVsync)); - EnableShaderCache = new ReactiveObject(); - EnableShaderCache.Event += static (_, e) => LogValueChange(e, nameof(EnableShaderCache)); - EnableTextureRecompression = new ReactiveObject(); - EnableTextureRecompression.Event += static (_, e) => LogValueChange(e, nameof(EnableTextureRecompression)); - GraphicsBackend = new ReactiveObject(); - GraphicsBackend.Event += static (_, e) => LogValueChange(e, nameof(GraphicsBackend)); - PreferredGpu = new ReactiveObject(); - PreferredGpu.Event += static (_, e) => LogValueChange(e, nameof(PreferredGpu)); - EnableMacroHLE = new ReactiveObject(); - EnableMacroHLE.Event += static (_, e) => LogValueChange(e, nameof(EnableMacroHLE)); - EnableColorSpacePassthrough = new ReactiveObject(); - EnableColorSpacePassthrough.Event += static (_, e) => LogValueChange(e, nameof(EnableColorSpacePassthrough)); - AntiAliasing = new ReactiveObject(); - AntiAliasing.Event += static (_, e) => LogValueChange(e, nameof(AntiAliasing)); - ScalingFilter = new ReactiveObject(); - ScalingFilter.Event += static (_, e) => LogValueChange(e, nameof(ScalingFilter)); - ScalingFilterLevel = new ReactiveObject(); - ScalingFilterLevel.Event += static (_, e) => LogValueChange(e, nameof(ScalingFilterLevel)); - } - } - - /// - /// Multiplayer configuration section - /// - public class MultiplayerSection - { - /// - /// GUID for the network interface used by LAN (or 0 for default) - /// - public ReactiveObject LanInterfaceId { get; private set; } - - /// - /// Multiplayer Mode - /// - public ReactiveObject Mode { get; private set; } - - public MultiplayerSection() - { - LanInterfaceId = new ReactiveObject(); - Mode = new ReactiveObject(); - Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode)); - } - } - - /// - /// The default configuration instance - /// - public static ConfigurationState Instance { get; private set; } - - /// - /// The UI section - /// - public UISection UI { get; private set; } - - /// - /// The Logger section - /// - public LoggerSection Logger { get; private set; } - - /// - /// The System section - /// - public SystemSection System { get; private set; } - - /// - /// The Graphics section - /// - public GraphicsSection Graphics { get; private set; } - - /// - /// The Hid section - /// - public HidSection Hid { get; private set; } - - /// - /// The Multiplayer section - /// - public MultiplayerSection Multiplayer { get; private set; } - - /// - /// Enables or disables Discord Rich Presence - /// - public ReactiveObject EnableDiscordIntegration { get; private set; } - - /// - /// Checks for updates when Ryujinx starts when enabled - /// - public ReactiveObject CheckUpdatesOnStart { get; private set; } - - /// - /// Show "Confirm Exit" Dialog - /// - public ReactiveObject ShowConfirmExit { get; private set; } - - /// - /// Ignore Applet - /// - public ReactiveObject IgnoreApplet { get; private set; } - - /// - /// Enables or disables save window size, position and state on close. - /// - public ReactiveObject RememberWindowState { get; private set; } - - /// - /// Enables or disables the redesigned title bar - /// - public ReactiveObject ShowTitleBar { get; private set; } - - /// - /// Enables hardware-accelerated rendering for Avalonia - /// - public ReactiveObject EnableHardwareAcceleration { get; private set; } - - /// - /// Hide Cursor on Idle - /// - public ReactiveObject HideCursor { get; private set; } - - private ConfigurationState() - { - UI = new UISection(); - Logger = new LoggerSection(); - System = new SystemSection(); - Graphics = new GraphicsSection(); - Hid = new HidSection(); - Multiplayer = new MultiplayerSection(); - EnableDiscordIntegration = new ReactiveObject(); - CheckUpdatesOnStart = new ReactiveObject(); - ShowConfirmExit = new ReactiveObject(); - IgnoreApplet = new ReactiveObject(); - RememberWindowState = new ReactiveObject(); - ShowTitleBar = new ReactiveObject(); - EnableHardwareAcceleration = new ReactiveObject(); - HideCursor = new ReactiveObject(); - } public ConfigurationFileFormat ToFileFormat() { @@ -711,7 +64,9 @@ namespace Ryujinx.UI.Common.Configuration ShowTitleBar = ShowTitleBar, EnableHardwareAcceleration = EnableHardwareAcceleration, HideCursor = HideCursor, - EnableVsync = Graphics.EnableVsync, + VSyncMode = Graphics.VSyncMode, + EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval, + CustomVSyncInterval = Graphics.CustomVSyncInterval, EnableShaderCache = Graphics.EnableShaderCache, EnableTextureRecompression = Graphics.EnableTextureRecompression, EnableMacroHLE = Graphics.EnableMacroHLE, @@ -734,6 +89,7 @@ namespace Ryujinx.UI.Common.Configuration AppColumn = UI.GuiColumns.AppColumn, DevColumn = UI.GuiColumns.DevColumn, VersionColumn = UI.GuiColumns.VersionColumn, + LdnInfoColumn = UI.GuiColumns.LdnInfoColumn, TimePlayedColumn = UI.GuiColumns.TimePlayedColumn, LastPlayedColumn = UI.GuiColumns.LastPlayedColumn, FileExtColumn = UI.GuiColumns.FileExtColumn, @@ -783,6 +139,9 @@ namespace Ryujinx.UI.Common.Configuration PreferredGpu = Graphics.PreferredGpu, MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId, MultiplayerMode = Multiplayer.Mode, + MultiplayerDisableP2p = Multiplayer.DisableP2p, + MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, + LdnServer = Multiplayer.LdnServer, }; return configurationFile; @@ -822,7 +181,9 @@ namespace Ryujinx.UI.Common.Configuration ShowTitleBar.Value = !OperatingSystem.IsWindows(); EnableHardwareAcceleration.Value = true; HideCursor.Value = HideCursorMode.OnIdle; - Graphics.EnableVsync.Value = true; + Graphics.VSyncMode.Value = VSyncMode.Switch; + Graphics.CustomVSyncInterval.Value = 120; + Graphics.EnableCustomVSyncInterval.Value = false; Graphics.EnableShaderCache.Value = true; Graphics.EnableTextureRecompression.Value = false; Graphics.EnableMacroHLE.Value = true; @@ -842,6 +203,9 @@ namespace Ryujinx.UI.Common.Configuration System.UseHypervisor.Value = true; Multiplayer.LanInterfaceId.Value = "0"; Multiplayer.Mode.Value = MultiplayerMode.Disabled; + Multiplayer.DisableP2p.Value = false; + Multiplayer.LdnPassphrase.Value = ""; + Multiplayer.LdnServer.Value = ""; UI.GuiColumns.FavColumn.Value = true; UI.GuiColumns.IconColumn.Value = true; UI.GuiColumns.AppColumn.Value = true; @@ -880,7 +244,7 @@ namespace Ryujinx.UI.Common.Configuration Hid.EnableMouse.Value = false; Hid.Hotkeys.Value = new KeyboardHotkeys { - ToggleVsync = Key.F1, + ToggleVSyncMode = Key.F1, ToggleMute = Key.F2, Screenshot = Key.F8, ShowUI = Key.F4, @@ -940,708 +304,9 @@ namespace Ryujinx.UI.Common.Configuration StickButton = Key.H, }, } - ]; } - public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath) - { - bool configurationFileUpdated = false; - - if (configurationFileFormat.Version is < 0 or > ConfigurationFileFormat.CurrentVersion) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default."); - - LoadDefault(); - } - - if (configurationFileFormat.Version < 2) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2."); - - configurationFileFormat.SystemRegion = Region.USA; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 3) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3."); - - configurationFileFormat.SystemTimeZone = "UTC"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 4) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4."); - - configurationFileFormat.MaxAnisotropy = -1; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 5) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5."); - - configurationFileFormat.SystemTimeOffset = 0; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 8) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8."); - - configurationFileFormat.EnablePtc = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 9) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9."); - - configurationFileFormat.ColumnSort = new ColumnSort - { - SortColumnId = 0, - SortAscending = false, - }; - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 10) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10."); - - configurationFileFormat.AudioBackend = AudioBackend.OpenAl; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 11) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11."); - - configurationFileFormat.ResScale = 1; - configurationFileFormat.ResScaleCustom = 1.0f; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 12) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12."); - - configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None; - - configurationFileUpdated = true; - } - - // configurationFileFormat.Version == 13 -> LDN1 - - if (configurationFileFormat.Version < 14) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14."); - - configurationFileFormat.CheckUpdatesOnStart = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 16) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16."); - - configurationFileFormat.EnableShaderCache = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 17) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17."); - - configurationFileFormat.StartFullscreen = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 18) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18."); - - configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9; - - configurationFileUpdated = true; - } - - // configurationFileFormat.Version == 19 -> LDN2 - - if (configurationFileFormat.Version < 20) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20."); - - configurationFileFormat.ShowConfirmExit = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 21) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21."); - - // Initialize network config. - - configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled; - configurationFileFormat.MultiplayerLanInterfaceId = "0"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 22) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22."); - - configurationFileFormat.HideCursor = HideCursorMode.Never; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 24) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24."); - - configurationFileFormat.InputConfig = new List - { - new StandardKeyboardInputConfig - { - Version = InputConfig.CurrentVersion, - Backend = InputBackendType.WindowKeyboard, - Id = "0", - PlayerIndex = PlayerIndex.Player1, - ControllerType = ControllerType.ProController, - LeftJoycon = new LeftJoyconCommonConfig - { - DpadUp = Key.Up, - DpadDown = Key.Down, - DpadLeft = Key.Left, - DpadRight = Key.Right, - ButtonMinus = Key.Minus, - ButtonL = Key.E, - ButtonZl = Key.Q, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - LeftJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = Key.W, - StickDown = Key.S, - StickLeft = Key.A, - StickRight = Key.D, - StickButton = Key.F, - }, - RightJoycon = new RightJoyconCommonConfig - { - ButtonA = Key.Z, - ButtonB = Key.X, - ButtonX = Key.C, - ButtonY = Key.V, - ButtonPlus = Key.Plus, - ButtonR = Key.U, - ButtonZr = Key.O, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound, - }, - RightJoyconStick = new JoyconConfigKeyboardStick - { - StickUp = Key.I, - StickDown = Key.K, - StickLeft = Key.J, - StickRight = Key.L, - StickButton = Key.H, - }, - }, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 25) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25."); - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 26) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26."); - - configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 27) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27."); - - configurationFileFormat.EnableMouse = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 28) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - Screenshot = Key.F8, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 29) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = Key.F1, - Screenshot = Key.F8, - ShowUI = Key.F4, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 30) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30."); - - foreach (InputConfig config in configurationFileFormat.InputConfig) - { - if (config is StandardControllerInputConfig controllerConfig) - { - controllerConfig.Rumble = new RumbleConfigController - { - EnableRumble = false, - StrongRumble = 1f, - WeakRumble = 1f, - }; - } - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 31) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31."); - - configurationFileFormat.BackendThreading = BackendThreading.Auto; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 32) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = Key.F5, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 33) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = Key.F2, - }; - - configurationFileFormat.AudioVolume = 1; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 34) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34."); - - configurationFileFormat.EnableInternetAccess = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 35) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35."); - - foreach (InputConfig config in configurationFileFormat.InputConfig) - { - if (config is StandardControllerInputConfig controllerConfig) - { - controllerConfig.RangeLeft = 1.0f; - controllerConfig.RangeRight = 1.0f; - } - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 36) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36."); - - configurationFileFormat.LoggingEnableTrace = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 37) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37."); - - configurationFileFormat.ShowConsole = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 38) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38."); - - configurationFileFormat.BaseStyle = "Dark"; - configurationFileFormat.GameListViewMode = 0; - configurationFileFormat.ShowNames = true; - configurationFileFormat.GridSize = 2; - configurationFileFormat.LanguageCode = "en_US"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 39) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, - ResScaleUp = Key.Unbound, - ResScaleDown = Key.Unbound, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 40) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40."); - - configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 41) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41."); - - configurationFileFormat.Hotkeys = new KeyboardHotkeys - { - ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync, - Screenshot = configurationFileFormat.Hotkeys.Screenshot, - ShowUI = configurationFileFormat.Hotkeys.ShowUI, - Pause = configurationFileFormat.Hotkeys.Pause, - ToggleMute = configurationFileFormat.Hotkeys.ToggleMute, - ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp, - ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown, - VolumeUp = Key.Unbound, - VolumeDown = Key.Unbound, - }; - } - - if (configurationFileFormat.Version < 42) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42."); - - configurationFileFormat.EnableMacroHLE = true; - } - - if (configurationFileFormat.Version < 43) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43."); - - configurationFileFormat.UseHypervisor = true; - } - - if (configurationFileFormat.Version < 44) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44."); - - configurationFileFormat.AntiAliasing = AntiAliasing.None; - configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear; - configurationFileFormat.ScalingFilterLevel = 80; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 45) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45."); - - configurationFileFormat.ShownFileTypes = new ShownFileTypes - { - NSP = true, - PFS0 = true, - XCI = true, - NCA = true, - NRO = true, - NSO = true, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 46) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46."); - - configurationFileFormat.MultiplayerLanInterfaceId = "0"; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 47) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47."); - - configurationFileFormat.WindowStartup = new WindowStartup - { - WindowPositionX = 0, - WindowPositionY = 0, - WindowSizeHeight = 760, - WindowSizeWidth = 1280, - WindowMaximized = false, - }; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 48) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48."); - - configurationFileFormat.EnableColorSpacePassthrough = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 49) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49."); - - if (OperatingSystem.IsMacOS()) - { - AppDataManager.FixMacOSConfigurationFolders(); - } - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 50) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 50."); - - configurationFileFormat.EnableHardwareAcceleration = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 51) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51."); - - configurationFileFormat.RememberWindowState = true; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 52) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52."); - - configurationFileFormat.AutoloadDirs = []; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 53) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53."); - - configurationFileFormat.EnableLowPowerPtc = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 54) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 54."); - - configurationFileFormat.DramSize = MemoryConfiguration.MemoryConfiguration4GiB; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 55) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 55."); - - configurationFileFormat.IgnoreApplet = false; - - configurationFileUpdated = true; - } - - if (configurationFileFormat.Version < 56) - { - Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 56."); - - configurationFileFormat.ShowTitleBar = !OperatingSystem.IsWindows(); - - configurationFileUpdated = true; - } - - Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; - Graphics.ResScale.Value = configurationFileFormat.ResScale; - Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; - Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy; - Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio; - Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath; - Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading; - Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend; - Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu; - Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing; - Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter; - Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel; - Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug; - Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub; - Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo; - Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn; - Logger.EnableError.Value = configurationFileFormat.LoggingEnableError; - Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace; - Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest; - Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog; - Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses; - Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel; - System.Language.Value = configurationFileFormat.SystemLanguage; - System.Region.Value = configurationFileFormat.SystemRegion; - System.TimeZone.Value = configurationFileFormat.SystemTimeZone; - System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset; - System.EnableDockedMode.Value = configurationFileFormat.DockedMode; - EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration; - CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart; - ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit; - IgnoreApplet.Value = configurationFileFormat.IgnoreApplet; - RememberWindowState.Value = configurationFileFormat.RememberWindowState; - ShowTitleBar.Value = configurationFileFormat.ShowTitleBar; - EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration; - HideCursor.Value = configurationFileFormat.HideCursor; - Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync; - Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache; - Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression; - Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE; - Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough; - System.EnablePtc.Value = configurationFileFormat.EnablePtc; - System.EnableLowPowerPtc.Value = configurationFileFormat.EnableLowPowerPtc; - System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess; - System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks; - System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode; - System.AudioBackend.Value = configurationFileFormat.AudioBackend; - System.AudioVolume.Value = configurationFileFormat.AudioVolume; - System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode; - System.DramSize.Value = configurationFileFormat.DramSize; - System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices; - System.UseHypervisor.Value = configurationFileFormat.UseHypervisor; - UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn; - UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn; - UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; - UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; - UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; - UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; - UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; - UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; - UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn; - UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn; - UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId; - UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending; - UI.GameDirs.Value = configurationFileFormat.GameDirs; - UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs ?? []; - UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP; - UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0; - UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI; - UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA; - UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO; - UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO; - UI.LanguageCode.Value = configurationFileFormat.LanguageCode; - UI.BaseStyle.Value = configurationFileFormat.BaseStyle; - UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode; - UI.ShowNames.Value = configurationFileFormat.ShowNames; - UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder; - UI.GridSize.Value = configurationFileFormat.GridSize; - UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort; - UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen; - UI.ShowConsole.Value = configurationFileFormat.ShowConsole; - UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth; - UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight; - UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX; - UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY; - UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized; - Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard; - Hid.EnableMouse.Value = configurationFileFormat.EnableMouse; - Hid.Hotkeys.Value = configurationFileFormat.Hotkeys; - Hid.InputConfig.Value = configurationFileFormat.InputConfig ?? []; - - Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId; - Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode; - - if (configurationFileUpdated) - { - ToFileFormat().SaveConfig(configurationFilePath); - - Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}"); - } - } - private static GraphicsBackend DefaultGraphicsBackend() { // Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan. @@ -1653,24 +318,5 @@ namespace Ryujinx.UI.Common.Configuration return GraphicsBackend.OpenGl; } - - private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName) - { - string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}"); - - Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, message); - } - - public static void Initialize() - { - if (Instance != null) - { - throw new InvalidOperationException("Configuration is already initialized"); } - - Instance = new ConfigurationState(); - - Instance.System.EnableLowPowerPtc.Event += (_, evnt) => Optimizations.LowPower = evnt.NewValue; } - } -} diff --git a/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs index 9cb283593..a7913f142 100644 --- a/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs +++ b/src/Ryujinx.UI.Common/Configuration/LoggerModule.cs @@ -1,4 +1,3 @@ -using Ryujinx.Common; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.Logging.Targets; @@ -11,103 +10,69 @@ namespace Ryujinx.UI.Common.Configuration { public static void Initialize() { - ConfigurationState.Instance.Logger.EnableDebug.Event += ReloadEnableDebug; - ConfigurationState.Instance.Logger.EnableStub.Event += ReloadEnableStub; - ConfigurationState.Instance.Logger.EnableInfo.Event += ReloadEnableInfo; - ConfigurationState.Instance.Logger.EnableWarn.Event += ReloadEnableWarning; - ConfigurationState.Instance.Logger.EnableError.Event += ReloadEnableError; - ConfigurationState.Instance.Logger.EnableTrace.Event += ReloadEnableTrace; - ConfigurationState.Instance.Logger.EnableGuest.Event += ReloadEnableGuest; - ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += ReloadEnableFsAccessLog; - ConfigurationState.Instance.Logger.FilteredClasses.Event += ReloadFilteredClasses; - ConfigurationState.Instance.Logger.EnableFileLog.Event += ReloadFileLogger; - } - - private static void ReloadEnableDebug(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Debug, e.NewValue); - } - - private static void ReloadEnableStub(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Stub, e.NewValue); - } - - private static void ReloadEnableInfo(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Info, e.NewValue); - } - - private static void ReloadEnableWarning(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Warning, e.NewValue); - } - - private static void ReloadEnableError(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Error, e.NewValue); - } - - private static void ReloadEnableTrace(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Trace, e.NewValue); - } - - private static void ReloadEnableGuest(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.Guest, e.NewValue); - } - - private static void ReloadEnableFsAccessLog(object sender, ReactiveEventArgs e) - { - Logger.SetEnable(LogLevel.AccessLog, e.NewValue); - } - - private static void ReloadFilteredClasses(object sender, ReactiveEventArgs e) - { - bool noFilter = e.NewValue.Length == 0; - - foreach (var logClass in Enum.GetValues()) + ConfigurationState.Instance.Logger.EnableDebug.Event += + (_, e) => Logger.SetEnable(LogLevel.Debug, e.NewValue); + ConfigurationState.Instance.Logger.EnableStub.Event += + (_, e) => Logger.SetEnable(LogLevel.Stub, e.NewValue); + ConfigurationState.Instance.Logger.EnableInfo.Event += + (_, e) => Logger.SetEnable(LogLevel.Info, e.NewValue); + ConfigurationState.Instance.Logger.EnableWarn.Event += + (_, e) => Logger.SetEnable(LogLevel.Warning, e.NewValue); + ConfigurationState.Instance.Logger.EnableError.Event += + (_, e) => Logger.SetEnable(LogLevel.Error, e.NewValue); + ConfigurationState.Instance.Logger.EnableTrace.Event += + (_, e) => Logger.SetEnable(LogLevel.Trace, e.NewValue); + ConfigurationState.Instance.Logger.EnableGuest.Event += + (_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue); + ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += + (_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue); + + ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) => { - Logger.SetEnable(logClass, noFilter); - } + bool noFilter = e.NewValue.Length == 0; - foreach (var logClass in e.NewValue) - { - Logger.SetEnable(logClass, true); - } - } - - private static void ReloadFileLogger(object sender, ReactiveEventArgs e) - { - if (e.NewValue) - { - string logDir = AppDataManager.LogsDirPath; - FileStream logFile = null; - - if (!string.IsNullOrEmpty(logDir)) + foreach (var logClass in Enum.GetValues()) { - logFile = FileLogTarget.PrepareLogFile(logDir); + Logger.SetEnable(logClass, noFilter); } - if (logFile == null) + foreach (var logClass in e.NewValue) + { + Logger.SetEnable(logClass, true); + } + }; + + ConfigurationState.Instance.Logger.EnableFileLog.Event += (_, e) => + { + if (e.NewValue) + { + string logDir = AppDataManager.LogsDirPath; + FileStream logFile = null; + + if (!string.IsNullOrEmpty(logDir)) + { + logFile = FileLogTarget.PrepareLogFile(logDir); + } + + if (logFile == null) + { + Logger.Error?.Print(LogClass.Application, + "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable."); + Logger.RemoveTarget("file"); + + return; + } + + Logger.AddTarget(new AsyncLogTargetWrapper( + new FileLogTarget("file", logFile), + 1000 + )); + } + else { - Logger.Error?.Print(LogClass.Application, "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable."); Logger.RemoveTarget("file"); - - return; } - - Logger.AddTarget(new AsyncLogTargetWrapper( - new FileLogTarget("file", logFile), - 1000, - AsyncLogTargetOverflowAction.Block - )); - } - else - { - Logger.RemoveTarget("file"); - } + }; } } } diff --git a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs index c778ef1f1..c486492e0 100644 --- a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs +++ b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs @@ -7,6 +7,7 @@ namespace Ryujinx.UI.Common.Configuration.UI public bool AppColumn { get; set; } public bool DevColumn { get; set; } public bool VersionColumn { get; set; } + public bool LdnInfoColumn { get; set; } public bool TimePlayedColumn { get; set; } public bool LastPlayedColumn { get; set; } public bool FileExtColumn { get; set; } diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs index 01781bab6..338d28531 100644 --- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -1,11 +1,10 @@ using DiscordRPC; using Humanizer; -using LibHac.Bcat; +using Humanizer.Localisation; using Ryujinx.Common; using Ryujinx.HLE.Loaders.Processes; using Ryujinx.UI.App.Common; using Ryujinx.UI.Common.Configuration; -using System.Collections.Generic; using System.Linq; using System.Text; @@ -15,9 +14,13 @@ namespace Ryujinx.UI.Common { public static Timestamps StartedAt { get; set; } - private static readonly string _description = ReleaseInformation.IsValid - ? $"v{ReleaseInformation.Version} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}@{ReleaseInformation.BuildGitHash}" - : "dev build"; + private static string VersionString + => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}"; + + private static readonly string _description = + ReleaseInformation.IsValid + ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}" + : "dev build"; private const string ApplicationId = "1293250299716173864"; @@ -74,13 +77,13 @@ namespace Ryujinx.UI.Common Assets = new Assets { LargeImageKey = _discordGameAssetKeys.Contains(procRes.ProgramIdText) ? procRes.ProgramIdText : "game", - LargeImageText = TruncateToByteLength($"{appMeta.Title} | {procRes.DisplayVersion}"), + LargeImageText = TruncateToByteLength($"{appMeta.Title} (v{procRes.DisplayVersion})"), SmallImageKey = "ryujinx", SmallImageText = TruncateToByteLength(_description) }, Details = TruncateToByteLength($"Playing {appMeta.Title}"), State = appMeta.LastPlayed.HasValue && appMeta.TimePlayed.TotalSeconds > 5 - ? $"Total play time: {appMeta.TimePlayed.Humanize(2, false)}" + ? $"Total play time: {appMeta.TimePlayed.Humanize(2, false, maxUnit: TimeUnit.Hour)}" : "Never played", Timestamps = Timestamps.Now }); @@ -122,70 +125,148 @@ namespace Ryujinx.UI.Common private static readonly string[] _discordGameAssetKeys = [ - "01002da013484000", // The Legend of Zelda: Skyward Sword HD + "010055d009f78000", // Fire Emblem: Three Houses + "0100a12011cc8000", // Fire Emblem: Shadow Dragon + "0100a6301214e000", // Fire Emblem Engage + "0100f15003e64000", // Fire Emblem Warriors + "010071f0143ea000", // Fire Emblem Warriors: Three Hopes + + "01007e3006dda000", // Kirby Star Allies + "01004d300c5ae000", // Kirby and the Forgotten Land + "01006b601380e000", // Kirby's Return to Dream Land Deluxe + "01003fb00c5a8000", // Super Kirby Clash + "0100227010460000", // Kirby Fighters 2 + "0100a8e016236000", // Kirby's Dream Buffet + "01007ef00011e000", // The Legend of Zelda: Breath of the Wild + "01006bb00c6f0000", // The Legend of Zelda: Link's Awakening + "01002da013484000", // The Legend of Zelda: Skyward Sword HD "0100f2c0115b6000", // The Legend of Zelda: Tears of the Kingdom "01008cf01baac000", // The Legend of Zelda: Echoes of Wisdom - "01006bb00c6f0000", // The Legend of Zelda: Link's Awakening - - "0100000000010000", // SUPER MARIO ODYSSEY - "010015100b514000", // Super Mario Bros. Wonder - "0100152000022000", // Mario Kart 8 Deluxe - "01006fe013472000", // Mario Party Superstars - "0100965017338000", // Super Mario Party Jamboree - "010049900f546000", // Super Mario 3D All-Stars - "010028600ebda000", // Super Mario 3D World + Bowser's Fury - "0100ecd018ebe000", // Paper Mario: The Thousand-Year Door - "010019401051c000", // Mario Strikers League - "0100ea80032ea000", // Super Mario Bros. U Deluxe - "0100bc0018138000", // Super Mario RPG - "0100bde00862a000", // Mario Tennis Aces + "01000b900d8b0000", // Cadence of Hyrule + "0100ae00096ea000", // Hyrule Warriors: Definitive Edition + "01002b00111a2000", // Hyrule Warriors: Age of Calamity "010048701995e000", // Luigi's Mansion 2 HD "0100dca0064a6000", // Luigi's Mansion 3 - "01008f6008c5e000", // Pokémon Violet - "0100abf008968000", // Pokémon Sword - "01008db008c2c000", // Pokémon Shield - "0100000011d90000", // Pokémon Brilliant Diamond - "01001f5010dfa000", // Pokémon Legends: Arceus + "010093801237c000", // Metroid Dread + "010012101468c000", // Metroid Prime Remastered + + "0100000000010000", // SUPER MARIO ODYSSEY + "0100ea80032ea000", // Super Mario Bros. U Deluxe + "01009b90006dc000", // Super Mario Maker 2 + "010049900f546000", // Super Mario 3D All-Stars + "010049900F546001", // ^ 64 + "010049900F546002", // ^ Sunshine + "010049900F546003", // ^ Galaxy + "010028600ebda000", // Super Mario 3D World + Bowser's Fury + "010015100b514000", // Super Mario Bros. Wonder + "0100152000022000", // Mario Kart 8 Deluxe + "010036b0034e4000", // Super Mario Party + "01006fe013472000", // Mario Party Superstars + "0100965017338000", // Super Mario Party Jamboree + "01006d0017f7a000", // Mario & Luigi: Brothership + "010067300059a000", // Mario + Rabbids: Kingdom Battle + "0100317013770000", // Mario + Rabbids: Sparks of Hope + "0100a3900c3e2000", // Paper Mario: The Origami King + "0100ecd018ebe000", // Paper Mario: The Thousand-Year Door + "0100bc0018138000", // Super Mario RPG + "0100bde00862a000", // Mario Tennis Aces + "0100c9c00e25c000", // Mario Golf: Super Rush + "010019401051c000", // Mario Strikers: Battle League + "010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020 + "0100b99019412000", // Mario vs. Donkey Kong "0100aa80194b0000", // Pikmin 1 "0100d680194b2000", // Pikmin 2 "0100f4c009322000", // Pikmin 3 Deluxe "0100b7c00933a000", // Pikmin 4 - + + "010003f003a34000", // Pokémon: Let's Go Pikachu! + "0100187003a36000", // Pokémon: Let's Go Eevee! + "0100abf008968000", // Pokémon Sword + "01008db008c2c000", // Pokémon Shield + "0100000011d90000", // Pokémon Brilliant Diamond + "010018e011d92000", // Pokémon Shining Pearl + "01001f5010dfa000", // Pokémon Legends: Arceus + "0100a3d008c5c000", // Pokémon Scarlet + "01008f6008c5e000", // Pokémon Violet + "0100b3f000be2000", // Pokkén Tournament DX + "0100f4300bf2c000", // New Pokémon Snap + + "01003bc0000a0000", // Splatoon 2 (US) + "0100f8f0000a2000", // Splatoon 2 (EU) + "01003c700009c000", // Splatoon 2 (JP) + "0100c2500fc20000", // Splatoon 3 + "0100ba0018500000", // Splatoon 3: Splatfest World Premiere + + "010040600c5ce000", // Tetris 99 + "0100277011f1a000", // Super Mario Bros. 35 + "0100ad9012510000", // PAC-MAN 99 + "0100ccf019c8c000", // F-ZERO 99 + "0100d870045b6000", // NES - Nintendo Switch Online + "01008d300c50c000", // SNES - Nintendo Switch Online + "0100c9a00ece6000", // N64 - Nintendo Switch Online + "0100e0601c632000", // N64 - Nintendo Switch Online 18+ + "0100c62011050000", // GB - Nintendo Switch Online + "010012f017576000", // GBA - Nintendo Switch Online + + "01000320000cc000", // 1-2 Switch + "0100300012f2a000", // Advance Wars 1+2: Re-Boot Camp + "01006f8002326000", // Animal Crossing: New Horizons + "0100620012d6e000", // Big Brain Academy: Brain vs. Brain + "010018300d006000", // BOXBOY! + BOXGIRL! + "0100c1f0051b6000", // Donkey Kong Country: Tropical Freeze + "0100ed000d390000", // Dr. Kawashima's Brain Training + "010067b017588000", // Endless Ocean Luminous + "0100d2f00d5c0000", // Nintendo Switch Sports + "01006b5012b32000", // Part Time UFO + "0100704000B3A000", // Snipperclips + "01006a800016e000", // Super Smash Bros. Ultimate + "0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore + + "010076f0049a2000", // Bayonetta + "01007960049a0000", // Bayonetta 2 + "01004a4010fea000", // Bayonetta 3 + "0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon + + "0100dcd01525a000", // Persona 3 Portable + "010062b01525c000", // Persona 4 Golden + "010075a016a3a000", // Persona 4 Arena Ultimax + "01005ca01580e000", // Persona 5 Royal + "0100801011c3e000", // Persona 5 Strikers + "010087701b092000", // Persona 5 Tactica + + "01009aa000faa000", // Sonic Mania "01004ad014bf0000", // Sonic Frontiers "01005ea01c0fc000", // SONIC X SHADOW GENERATIONS "01005ea01c0fc001", // ^ - - "01004d300c5ae000", // Kirby and the Forgotten Land - "01006b601380e000", // Kirby's Return to Dreamland Deluxe - "01007e3006dda000", // Kirby Star Allies - "0100c2500fc20000", // Splatoon 3 - "0100ba0018500000", // Splatoon 3: Splatfest World Premiere - "01000a10041ea000", // The Elder Scrolls V: Skyrim - "01007820196a6000", // Red Dead Redemption - "01008c8012920000", // Dying Light Platinum Edition - "0100744001588000", // Cars 3: Driven to Win - "0100c1f0051b6000", // Donkey Kong Country: Tropical Freeze - "01002b00111a2000", // Hyrule Warriors: Age of Calamity - "01006f8002326000", // Animal Crossing: New Horizons - "0100853015e86000", // No Man's Sky - "01008d100d43e000", // Saints Row IV - "0100de600beee000", // Saints Row: The Third - The Full Package - "0100d7a01b7a2000", // Star Wars: Bounty Hunter - "0100dbf01000a000", // Burnout Paradise Remastered - "0100e46006708000", // Terraria "010056e00853a000", // A Hat in Time - "01006a800016e000", // Super Smash Bros. Ultimate + "0100dbf01000a000", // Burnout Paradise Remastered + "0100744001588000", // Cars 3: Driven to Win + "0100b41013c82000", // Cruis'n Blast + "01001b300b9be000", // Diablo III: Eternal Collection + "01008c8012920000", // Dying Light Platinum Edition + "010073c01af34000", // LEGO Horizon Adventures + "0100770008dd8000", // Monster Hunter Generations Ultimate + "0100b04011742000", // Monster Hunter Rise + "0100853015e86000", // No Man's Sky "01007bb017812000", // Portal "0100abd01785c000", // Portal 2 "01008e200c5c2000", // Muse Dash + "01007820196a6000", // Red Dead Redemption + "01002f7013224000", // Rune Factory 5 + "01008d100d43e000", // Saints Row IV + "0100de600beee000", // Saints Row: The Third - The Full Package "01001180021fa000", // Shovel Knight: Specter of Torment - "010012101468c000", // Metroid Prime Remastered - "0100c9a00ece6000", // Nintendo 64 - Nintendo Switch Online + "0100d7a01b7a2000", // Star Wars: Bounty Hunter + "0100800015926000", // Suika Game + "0100e46006708000", // Terraria + "01000a10041ea000", // The Elder Scrolls V: Skyrim + "010057a01e4d4000", // TSUKIHIME -A piece of blue glass moon- + "010080b00ad66000", // Undertale ]; } } diff --git a/src/Ryujinx.UI.Common/Helper/CommandLineState.cs b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs index ae0e4d904..3a96a55c8 100644 --- a/src/Ryujinx.UI.Common/Helper/CommandLineState.cs +++ b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs @@ -16,6 +16,7 @@ namespace Ryujinx.UI.Common.Helper public static string LaunchPathArg { get; private set; } public static string LaunchApplicationId { get; private set; } public static bool StartFullscreenArg { get; private set; } + public static bool HideAvailableUpdates { get; private set; } public static void ParseArguments(string[] args) { @@ -93,6 +94,9 @@ namespace Ryujinx.UI.Common.Helper OverrideHideCursor = args[++i]; break; + case "--hide-updates": + HideAvailableUpdates = true; + break; case "--software-gui": OverrideHardwareAcceleration = false; break; diff --git a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs index 93b2d6138..9333a1b76 100644 --- a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs @@ -4,6 +4,7 @@ using Ryujinx.Common.Logging; using System; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -23,6 +24,26 @@ namespace Ryujinx.UI.Common.Helper public static partial void SHChangeNotify(uint wEventId, uint uFlags, nint dwItem1, nint dwItem2); public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) && !ReleaseInformation.IsFlatHubBuild; + + public static bool AreMimeTypesRegistered + { + get + { + if (OperatingSystem.IsLinux()) + { + return AreMimeTypesRegisteredLinux(); + } + + if (OperatingSystem.IsWindows()) + { + return AreMimeTypesRegisteredWindows(); + } + + // TODO: Add macOS support. + + return false; + } + } [SupportedOSPlatform("linux")] private static bool AreMimeTypesRegisteredLinux() => File.Exists(Path.Combine(_mimeDbPath, "packages", "Ryujinx.xml")); @@ -72,35 +93,39 @@ namespace Ryujinx.UI.Common.Helper [SupportedOSPlatform("windows")] private static bool AreMimeTypesRegisteredWindows() { + return _fileExtensions.Aggregate(false, + (current, ext) => current | CheckRegistering(ext) + ); + static bool CheckRegistering(string ext) { RegistryKey key = Registry.CurrentUser.OpenSubKey(@$"Software\Classes\{ext}"); - if (key is null) + var openCmd = key?.OpenSubKey(@"shell\open\command"); + + if (openCmd is null) { return false; } - - var openCmd = key.OpenSubKey(@"shell\open\command"); - + string keyValue = (string)openCmd.GetValue(string.Empty); return keyValue is not null && (keyValue.Contains("Ryujinx") || keyValue.Contains(AppDomain.CurrentDomain.FriendlyName)); } - - bool registered = false; - - foreach (string ext in _fileExtensions) - { - registered |= CheckRegistering(ext); - } - - return registered; } [SupportedOSPlatform("windows")] private static bool InstallWindowsMimeTypes(bool uninstall = false) { + bool registered = _fileExtensions.Aggregate(false, + (current, ext) => current | RegisterExtension(ext, uninstall) + ); + + // Notify Explorer the file association has been changed. + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, nint.Zero, nint.Zero); + + return registered; + static bool RegisterExtension(string ext, bool uninstall = false) { string keyString = @$"Software\Classes\{ext}"; @@ -127,42 +152,13 @@ namespace Ryujinx.UI.Common.Helper Logger.Debug?.Print(LogClass.Application, $"Adding type association {ext}"); using var openCmd = key.CreateSubKey(@"shell\open\command"); - openCmd.SetValue("", $"\"{Environment.ProcessPath}\" \"%1\""); + openCmd.SetValue(string.Empty, $"\"{Environment.ProcessPath}\" \"%1\""); Logger.Debug?.Print(LogClass.Application, $"Added type association {ext}"); } return true; } - - bool registered = false; - - foreach (string ext in _fileExtensions) - { - registered |= RegisterExtension(ext, uninstall); - } - - // Notify Explorer the file association has been changed. - SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, nint.Zero, nint.Zero); - - return registered; - } - - public static bool AreMimeTypesRegistered() - { - if (OperatingSystem.IsLinux()) - { - return AreMimeTypesRegisteredLinux(); - } - - if (OperatingSystem.IsWindows()) - { - return AreMimeTypesRegisteredWindows(); - } - - // TODO: Add macOS support. - - return false; } public static bool Install() diff --git a/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonResponse.cs b/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonResponse.cs index 0250e1094..7bec1bcdc 100644 --- a/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonResponse.cs +++ b/src/Ryujinx.UI.Common/Models/Github/GithubReleasesJsonResponse.cs @@ -5,6 +5,8 @@ namespace Ryujinx.UI.Common.Models.Github public class GithubReleasesJsonResponse { public string Name { get; set; } + + public string TagName { get; set; } public List Assets { get; set; } } } diff --git a/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs b/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs new file mode 100644 index 000000000..95fb3985b --- /dev/null +++ b/src/Ryujinx.UI.Common/Models/XCITrimmerFileModel.cs @@ -0,0 +1,55 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.UI.App.Common; + +namespace Ryujinx.UI.Common.Models +{ + public record XCITrimmerFileModel( + string Name, + string Path, + bool Trimmable, + bool Untrimmable, + long PotentialSavingsB, + long CurrentSavingsB, + int? PercentageProgress, + XCIFileTrimmer.OperationOutcome ProcessingOutcome) + { + public static XCITrimmerFileModel FromApplicationData(ApplicationData applicationData, XCIFileTrimmerLog logger) + { + var trimmer = new XCIFileTrimmer(applicationData.Path, logger); + + return new XCITrimmerFileModel( + applicationData.Name, + applicationData.Path, + trimmer.CanBeTrimmed, + trimmer.CanBeUntrimmed, + trimmer.DiskSpaceSavingsB, + trimmer.DiskSpaceSavedB, + null, + XCIFileTrimmer.OperationOutcome.Undetermined + ); + } + + public bool IsFailed + { + get + { + return ProcessingOutcome != XCIFileTrimmer.OperationOutcome.Undetermined && + ProcessingOutcome != XCIFileTrimmer.OperationOutcome.Successful; + } + } + + public virtual bool Equals(XCITrimmerFileModel obj) + { + if (obj == null) + return false; + + return this.Path == obj.Path; + } + + public override int GetHashCode() + { + return this.Path.GetHashCode(); + } + } +} diff --git a/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj b/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj index df6532a63..7f57c7bf5 100644 --- a/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj +++ b/src/Ryujinx.UI.Common/Ryujinx.UI.Common.csproj @@ -3,6 +3,7 @@ net8.0 true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx.UI.LocaleGenerator/Ryujinx.UI.LocaleGenerator.csproj b/src/Ryujinx.UI.LocaleGenerator/Ryujinx.UI.LocaleGenerator.csproj index 05cbc7644..e4e627072 100644 --- a/src/Ryujinx.UI.LocaleGenerator/Ryujinx.UI.LocaleGenerator.csproj +++ b/src/Ryujinx.UI.LocaleGenerator/Ryujinx.UI.LocaleGenerator.csproj @@ -5,6 +5,7 @@ enable latest true + $(DefaultItemExcludes);._* diff --git a/src/Ryujinx/App.axaml.cs b/src/Ryujinx/App.axaml.cs index 509deb34c..15ada201c 100644 --- a/src/Ryujinx/App.axaml.cs +++ b/src/Ryujinx/App.axaml.cs @@ -23,8 +23,10 @@ namespace Ryujinx.Ava { internal static string FormatTitle(LocaleKeys? windowTitleKey = null) => windowTitleKey is null - ? $"Ryujinx {Program.Version}" - : $"Ryujinx {Program.Version} - {LocaleManager.Instance[windowTitleKey.Value]}"; + ? $"{FullAppName} {Program.Version}" + : $"{FullAppName} {Program.Version} - {LocaleManager.Instance[windowTitleKey.Value]}"; + + public static readonly string FullAppName = ReleaseInformation.IsCanaryBuild ? "Ryujinx Canary" : "Ryujinx"; public static MainWindow MainWindow => Current! .ApplicationLifetime.Cast() diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index dc4f4ff36..5789737d6 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -57,6 +57,8 @@ using Key = Ryujinx.Input.Key; using MouseButton = Ryujinx.Input.MouseButton; using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter; using Size = Avalonia.Size; +using Switch = Ryujinx.HLE.Switch; +using VSyncMode = Ryujinx.Common.Configuration.VSyncMode; namespace Ryujinx.Ava { @@ -203,10 +205,16 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter; ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel; ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough; + ConfigurationState.Instance.Graphics.VSyncMode.Event += UpdateVSyncMode; + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Event += UpdateCustomVSyncIntervalValue; + ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Event += UpdateCustomVSyncIntervalEnabled; ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState; ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState; ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState; + ConfigurationState.Instance.Multiplayer.LdnPassphrase.Event += UpdateLdnPassphraseState; + ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState; + ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState; _gpuCancellationTokenSource = new CancellationTokenSource(); _gpuDoneEvent = new ManualResetEvent(false); @@ -292,6 +300,66 @@ namespace Ryujinx.Ava _renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value); } + public void UpdateVSyncMode(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.VSyncMode = e.NewValue; + Device.UpdateVSyncInterval(); + } + _renderer.Window?.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)e.NewValue); + + _viewModel.ShowCustomVSyncIntervalPicker = (e.NewValue == VSyncMode.Custom); + } + + public void VSyncModeToggle() + { + VSyncMode oldVSyncMode = Device.VSyncMode; + VSyncMode newVSyncMode = VSyncMode.Switch; + bool customVSyncIntervalEnabled = ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Value; + + switch (oldVSyncMode) + { + case VSyncMode.Switch: + newVSyncMode = VSyncMode.Unbounded; + break; + case VSyncMode.Unbounded: + if (customVSyncIntervalEnabled) + { + newVSyncMode = VSyncMode.Custom; + } + else + { + newVSyncMode = VSyncMode.Switch; + } + + break; + case VSyncMode.Custom: + newVSyncMode = VSyncMode.Switch; + break; + } + + UpdateVSyncMode(this, new ReactiveEventArgs(oldVSyncMode, newVSyncMode)); + } + + private void UpdateCustomVSyncIntervalValue(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.TargetVSyncInterval = e.NewValue; + Device.UpdateVSyncInterval(); + } + } + + private void UpdateCustomVSyncIntervalEnabled(object sender, ReactiveEventArgs e) + { + if (Device != null) + { + Device.CustomVSyncIntervalEnabled = e.NewValue; + Device.UpdateVSyncInterval(); + } + } + private void ShowCursor() { Dispatcher.UIThread.Post(() => @@ -349,11 +417,7 @@ namespace Ryujinx.Ava string filename = $"{sanitizedApplicationName}_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png"; - string directory = AppDataManager.Mode switch - { - AppDataManager.LaunchMode.Portable or AppDataManager.LaunchMode.Custom => Path.Combine(AppDataManager.BaseDirPath, "screenshots"), - _ => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx"), - }; + string directory = Path.Combine(AppDataManager.BaseDirPath, "screenshots"); string path = Path.Combine(directory, filename); @@ -491,10 +555,19 @@ namespace Ryujinx.Ava Device.Configuration.MultiplayerMode = e.NewValue; } - public void ToggleVSync() + private void UpdateLdnPassphraseState(object sender, ReactiveEventArgs e) { - Device.EnableDeviceVsync = !Device.EnableDeviceVsync; - _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + Device.Configuration.MultiplayerLdnPassphrase = e.NewValue; + } + + private void UpdateLdnServerState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerLdnServer = e.NewValue; + } + + private void UpdateDisableP2pState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerDisableP2p = e.NewValue; } public void Stop() @@ -850,7 +923,7 @@ namespace Ryujinx.Ava _viewModel.UiHandler, (SystemLanguage)ConfigurationState.Instance.System.Language.Value, (RegionCode)ConfigurationState.Instance.System.Region.Value, - ConfigurationState.Instance.Graphics.EnableVsync, + ConfigurationState.Instance.Graphics.VSyncMode, ConfigurationState.Instance.System.EnableDockedMode, ConfigurationState.Instance.System.EnablePtc, ConfigurationState.Instance.System.EnableInternetAccess, @@ -863,10 +936,12 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Graphics.AspectRatio, ConfigurationState.Instance.System.AudioVolume, ConfigurationState.Instance.System.UseHypervisor, - ConfigurationState.Instance.Multiplayer.LanInterfaceId, - ConfigurationState.Instance.Multiplayer.Mode - ) - ); + ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, + ConfigurationState.Instance.Multiplayer.Mode, + ConfigurationState.Instance.Multiplayer.DisableP2p, + ConfigurationState.Instance.Multiplayer.LdnPassphrase, + ConfigurationState.Instance.Multiplayer.LdnServer, + ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value)); } private static IHardwareDeviceDriver InitializeAudio() @@ -987,7 +1062,7 @@ namespace Ryujinx.Ava Device.Gpu.SetGpuThread(); Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token); - _renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync); + _renderer.Window.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)Device.VSyncMode); while (_isActive) { @@ -1048,16 +1123,17 @@ namespace Ryujinx.Ava { // Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued. string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld]; + string vSyncMode = Device.VSyncMode.ToString(); UpdateShaderCount(); - + if (GraphicsConfig.ResScale != 1) { dockedMode += $" ({GraphicsConfig.ResScale}x)"; } StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( - Device.EnableDeviceVsync, + vSyncMode, LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%", dockedMode, ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(), @@ -1160,8 +1236,16 @@ namespace Ryujinx.Ava { switch (currentHotkeyState) { - case KeyboardHotkeyState.ToggleVSync: - ToggleVSync(); + case KeyboardHotkeyState.ToggleVSyncMode: + VSyncModeToggle(); + break; + case KeyboardHotkeyState.CustomVSyncIntervalDecrement: + Device.DecrementCustomVSyncInterval(); + _viewModel.CustomVSyncInterval -= 1; + break; + case KeyboardHotkeyState.CustomVSyncIntervalIncrement: + Device.IncrementCustomVSyncInterval(); + _viewModel.CustomVSyncInterval += 1; break; case KeyboardHotkeyState.Screenshot: ScreenshotRequested = true; @@ -1248,9 +1332,9 @@ namespace Ryujinx.Ava { KeyboardHotkeyState state = KeyboardHotkeyState.None; - if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync)) + if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVSyncMode)) { - state = KeyboardHotkeyState.ToggleVSync; + state = KeyboardHotkeyState.ToggleVSyncMode; } else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot)) { @@ -1284,6 +1368,14 @@ namespace Ryujinx.Ava { state = KeyboardHotkeyState.VolumeDown; } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalIncrement)) + { + state = KeyboardHotkeyState.CustomVSyncIntervalIncrement; + } + else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalDecrement)) + { + state = KeyboardHotkeyState.CustomVSyncIntervalDecrement; + } return state; } diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json index 22e270901..c1ee30f19 100644 --- a/src/Ryujinx/Assets/Locales/ar_SA.json +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -1,6 +1,7 @@ { "Language": "اَلْعَرَبِيَّةُ", "MenuBarFileOpenApplet": "فتح التطبيق المصغر", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "‫افتح تطبيق تحرير Mii في الوضع المستقل", "SettingsTabInputDirectMouseAccess": "الوصول المباشر للفأرة", "SettingsTabSystemMemoryManagerMode": "وضع إدارة الذاكرة:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "استخدم مراقب الأجهزة الافتراضية", "MenuBarFile": "_ملف", "MenuBarFileOpenFromFile": "_تحميل تطبيق من ملف", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "تحميل لُعْبَة غير محزومة", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "‫فتح مجلد Ryujinx", "MenuBarFileOpenLogsFolder": "فتح مجلد السجلات", "MenuBarFileExit": "_خروج", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت", "MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "تثبيت برنامج ثابت من مجلد", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "إدارة أنواع الملفات", "MenuBarToolsInstallFileTypes": "تثبيت أنواع الملفات", "MenuBarToolsUninstallFileTypes": "إزالة أنواع الملفات", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_عرض", "MenuBarViewWindow": "حجم النافذة", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "يفتح المجلد الذي يحتوي على تعديلات‫(mods) التطبيق", "GameListContextMenuOpenSdModsDirectory": "فتح مجلد تعديلات‫(mods) أتموسفير", "GameListContextMenuOpenSdModsDirectoryToolTip": "يفتح مجلد أتموسفير لبطاقة SD البديلة الذي يحتوي على تعديلات التطبيق. مفيد للتعديلات التي تم تعبئتها للأجهزة الحقيقية.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} لعبة تم تحميلها", "StatusBarSystemVersion": "إصدار النظام: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "الحد الأدنى لتعيينات الذاكرة المكتشفة", "LinuxVmMaxMapCountDialogTextPrimary": "هل ترغب في زيادة قيمة vm.max_map_count إلى {0}", "LinuxVmMaxMapCountDialogTextSecondary": "قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيغلق ريوجينكس بمجرد تجاوز هذا الحد.", @@ -103,6 +114,8 @@ "SettingsTabGeneralHideCursorOnIdle": "عند الخمول", "SettingsTabGeneralHideCursorAlways": "دائما", "SettingsTabGeneralGameDirectories": "مجلدات الألعاب", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "إضافة", "SettingsTabGeneralRemove": "إزالة", "SettingsTabSystem": "النظام", @@ -395,6 +408,8 @@ "InputDialogTitle": "حوار الإدخال", "InputDialogOk": "موافق", "InputDialogCancel": "إلغاء", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "اختر اسم الملف الشخصي", "InputDialogAddNewProfileHeader": "الرجاء إدخال اسم الملف الشخصي", "InputDialogAddNewProfileSubtext": "(الطول الأقصى: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "تعيين لون الخلفية", "AvatarClose": "إغلاق", "ControllerSettingsLoadProfileToolTip": "تحميل الملف الشخصي", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "إضافة ملف شخصي", "ControllerSettingsRemoveProfileToolTip": "إزالة الملف الشخصي", "ControllerSettingsSaveProfileToolTip": "حفظ الملف الشخصي", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "تعيين كمفضل", "GameListContextMenuToggleFavoriteToolTip": "تبديل الحالة المفضلة للعبة", "SettingsTabGeneralTheme": "السمة:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "داكن", "SettingsTabGeneralThemeLight": "فاتح", "ControllerSettingsConfigureGeneral": "ضبط", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "حدث خطأ أثناء البحث عن بيانات الحفظ المحددة: {0}", "FolderDialogExtractTitle": "اختر المجلد الذي تريد الاستخراج إليه", "DialogNcaExtractionMessage": "استخراج قسم {0} من {1}...", - "DialogNcaExtractionTitle": "ريوجينكس - مستخرج قسم NCA", + "DialogNcaExtractionTitle": "مستخرج قسم NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "فشل الاستخراج. لم يكن NCA الرئيسي موجودا في الملف المحدد.", "DialogNcaExtractionCheckLogErrorMessage": "فشل الاستخراج. اقرأ ملف التسجيل لمزيد من المعلومات.", "DialogNcaExtractionSuccessMessage": "تم الاستخراج بنجاح.", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "جاري استخراج التحديث...", "DialogUpdaterRenamingMessage": "إعادة تسمية التحديث...", "DialogUpdaterAddingFilesMessage": "إضافة تحديث جديد...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "اكتمل التحديث", "DialogUpdaterRestartMessage": "هل تريد إعادة تشغيل ريوجينكس الآن؟", "DialogUpdaterNoInternetMessage": "أنت غير متصل بالإنترنت.", "DialogUpdaterNoInternetSubMessage": "يرجى التحقق من أن لديك اتصال إنترنت فعال!", "DialogUpdaterDirtyBuildMessage": "لا يمكنك تحديث نسخة القذرة من ريوجينكس!", - "DialogUpdaterDirtyBuildSubMessage": "الرجاء تحميل ريوجينكس من https://https://github.com/GreemDev/Ryujinx/releases إذا كنت تبحث عن إصدار مدعوم.", + "DialogUpdaterDirtyBuildSubMessage": "الرجاء تحميل ريوجينكس من https://ryujinx.app/download إذا كنت تبحث عن إصدار مدعوم.", "DialogRestartRequiredMessage": "يتطلب إعادة التشغيل", "DialogThemeRestartMessage": "تم حفظ السمة. إعادة التشغيل مطلوبة لتطبيق السمة.", "DialogThemeRestartSubMessage": "هل تريد إعادة التشغيل", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "تم إلغاء تثبيت أنواع الملفات بنجاح!", "DialogUninstallFileTypesErrorMessage": "فشل إلغاء تثبيت أنواع الملفات.", "DialogOpenSettingsWindowLabel": "فتح نافذة الإعدادات", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "تطبيق وحدة التحكم المصغر", "DialogMessageDialogErrorExceptionMessage": "خطأ في عرض مربع حوار الرسالة: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "خطأ في عرض لوحة مفاتيح البرامج: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\nهل تريد المتابعة؟", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "تثبيت البرنامج الثابت...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "تم تثبيت إصدار النظام {0} بنجاح.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "لن تكون هناك ملفات الشخصية أخرى لفتحها إذا تم حذف الملف الشخصي المحدد", "DialogUserProfileDeletionConfirmMessage": "هل تريد حذف الملف الشخصي المحدد", "DialogUserProfileUnsavedChangesTitle": "تحذير - التغييرات غير محفوظة", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "أدخل مجلد اللعبة لإضافته إلى القائمة", "AddGameDirTooltip": "إضافة مجلد اللعبة إلى القائمة", "RemoveGameDirTooltip": "إزالة مجلد اللعبة المحدد", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "استخدم سمة أفالونيا المخصصة لواجهة المستخدم الرسومية لتغيير مظهر قوائم المحاكي", "CustomThemePathTooltip": "مسار سمة واجهة المستخدم المخصصة", "CustomThemeBrowseTooltip": "تصفح للحصول على سمة واجهة المستخدم المخصصة", @@ -606,6 +635,8 @@ "DebugLogTooltip": "طباعة رسائل سجل التصحيح في وحدة التحكم.\n\nاستخدم هذا فقط إذا طلب منك أحد الموظفين تحديدًا ذلك، لأنه سيجعل من الصعب قراءة السجلات وسيؤدي إلى تدهور أداء المحاكي.", "LoadApplicationFileTooltip": "افتح مستكشف الملفات لاختيار ملف متوافق مع سويتش لتحميله", "LoadApplicationFolderTooltip": "افتح مستكشف الملفات لاختيار تطبيق متوافق مع سويتش للتحميل", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "فتح مجلد نظام ملفات ريوجينكس", "OpenRyujinxLogsTooltip": "يفتح المجلد الذي تتم كتابة السجلات إليه", "ExitTooltip": "الخروج من ريوجينكس", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "فتح دليل الإعداد", "NoUpdate": "لا يوجد تحديث", "TitleUpdateVersionLabel": "الإصدار: {0}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "ريوجينكس - معلومات", "RyujinxConfirm": "ريوجينكس - تأكيد", "FileDialogAllTypes": "كل الأنواع", "Never": "مطلقا", "SwkbdMinCharacters": "يجب أن يبلغ طوله {0} حرفا على الأقل", "SwkbdMinRangeCharacters": "يجب أن يتكون من {0}-{1} حرفا", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "لوحة المفاتيح البرمجية", "SoftwareKeyboardModeNumeric": "يجب أن يكون 0-9 أو '.' فقط", "SoftwareKeyboardModeAlphabet": "يجب أن تكون الأحرف غير CJK فقط", @@ -709,16 +751,53 @@ "SelectDlcDialogTitle": "حدد ملفات المحتوي الإضافي", "SelectUpdateDialogTitle": "حدد ملفات التحديث", "SelectModDialogTitle": "حدد مجلد التعديل", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "مدير الملفات الشخصية للمستخدمين", "CheatWindowTitle": "مدير الغش", "DlcWindowTitle": "إدارة المحتوى القابل للتنزيل لـ {0} ({1})", "ModWindowTitle": "إدارة التعديلات لـ {0} ({1})", "UpdateWindowTitle": "مدير تحديث العنوان", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "الغش متوفر لـ {0} [{1}]", "BuildId": "معرف البناء:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "DlcWindowHeading": "المحتويات القابلة للتنزيل {0}", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} تعديل", "UserProfilesEditProfile": "تعديل المحدد", + "Continue": "Continue", "Cancel": "إلغاء", "Save": "حفظ", "Discard": "تجاهل", @@ -767,6 +846,7 @@ "GraphicsScalingFilterBilinear": "Bilinear", "GraphicsScalingFilterNearest": "Nearest", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "المستوى", "GraphicsScalingFilterLevelTooltip": "اضبط مستوى وضوح FSR 1.0. الأعلى هو أكثر وضوحا.", "SmaaLow": "SMAA منخفض", @@ -785,5 +865,17 @@ "MultiplayerMode": "الوضع:", "MultiplayerModeTooltip": "تغيير وضع LDN متعدد اللاعبين.\n\nسوف يقوم LdnMitm بتعديل وظيفة اللعب المحلية/اللاسلكية المحلية في الألعاب لتعمل كما لو كانت شبكة LAN، مما يسمح باتصالات الشبكة المحلية نفسها مع محاكيات ريوجينكس الأخرى وأجهزة نينتندو سويتش المخترقة التي تم تثبيت وحدة ldn_mitm عليها.\n\nيتطلب وضع اللاعبين المتعددين أن يكون جميع اللاعبين على نفس إصدار اللعبة (على سبيل المثال، يتعذر على الإصدار 13.0.1 من سوبر سماش برذرز ألتميت الاتصال بالإصدار 13.0.0).\n\nاتركه معطلا إذا لم تكن متأكدا.", "MultiplayerModeDisabled": "معطل", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json index 94e372e2e..e3f6b1be1 100644 --- a/src/Ryujinx/Assets/Locales/de_DE.json +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -1,6 +1,7 @@ { "Language": "Deutsch", "MenuBarFileOpenApplet": "Öffne Anwendung", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Öffnet das Mii-Editor-Applet im Standalone-Modus", "SettingsTabInputDirectMouseAccess": "Direkter Mauszugriff", "SettingsTabSystemMemoryManagerMode": "Speichermanagermodus:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "Hypervisor verwenden", "MenuBarFile": "_Datei", "MenuBarFileOpenFromFile": "Datei _öffnen", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "_Entpacktes Spiel öffnen", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "Ryujinx-Ordner öffnen", "MenuBarFileOpenLogsFolder": "Logs-Ordner öffnen", "MenuBarFileExit": "_Beenden", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "Firmware installieren", "MenuBarFileToolsInstallFirmwareFromFile": "Firmware von einer XCI- oder einer ZIP-Datei installieren", "MenuBarFileToolsInstallFirmwareFromDirectory": "Firmware aus einem Verzeichnis installieren", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Dateitypen verwalten", "MenuBarToolsInstallFileTypes": "Dateitypen installieren", "MenuBarToolsUninstallFileTypes": "Dateitypen deinstallieren", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_Ansicht", "MenuBarViewWindow": "Fenstergröße", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Öffnet das Verzeichnis, welches Mods für die Spiele beinhaltet", "GameListContextMenuOpenSdModsDirectory": "Atmosphere-Mod-Verzeichnis öffnen", "GameListContextMenuOpenSdModsDirectoryToolTip": "Öffnet das alternative SD-Karten-Atmosphere-Verzeichnis, das die Mods der Anwendung enthält. Dieser Ordner ist nützlich für Mods, die für echte Hardware erstellt worden sind.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} Spiele geladen", "StatusBarSystemVersion": "Systemversion: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Niedriges Limit für Speicherzuordnungen erkannt", "LinuxVmMaxMapCountDialogTextPrimary": "Möchtest Du den Wert von vm.max_map_count auf {0} erhöhen", "LinuxVmMaxMapCountDialogTextSecondary": "Einige Spiele könnten versuchen, mehr Speicherzuordnungen zu erstellen, als derzeit erlaubt. Ryujinx wird abstürzen, sobald dieses Limit überschritten wird.", @@ -103,6 +114,8 @@ "SettingsTabGeneralHideCursorOnIdle": "Mauszeiger bei Inaktivität ausblenden", "SettingsTabGeneralHideCursorAlways": "Immer", "SettingsTabGeneralGameDirectories": "Spielverzeichnisse", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "Hinzufügen", "SettingsTabGeneralRemove": "Entfernen", "SettingsTabSystem": "System", @@ -395,6 +408,8 @@ "InputDialogTitle": "Eingabe-Dialog", "InputDialogOk": "OK", "InputDialogCancel": "Abbrechen", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "Wähle den Profilnamen", "InputDialogAddNewProfileHeader": "Bitte gebe einen Profilnamen ein", "InputDialogAddNewProfileSubtext": "(Maximale Länge: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "Hintergrundfarbe auswählen", "AvatarClose": "Schließen", "ControllerSettingsLoadProfileToolTip": "Lädt ein Profil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Fügt ein Profil hinzu", "ControllerSettingsRemoveProfileToolTip": "Entfernt ein Profil", "ControllerSettingsSaveProfileToolTip": "Speichert ein Profil", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "Als Favoriten hinzufügen/entfernen", "GameListContextMenuToggleFavoriteToolTip": "Aktiviert den Favoriten-Status des Spiels", "SettingsTabGeneralTheme": "Design:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "Dunkel", "SettingsTabGeneralThemeLight": "Hell", "ControllerSettingsConfigureGeneral": "Konfigurieren", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "Es ist ein Fehler beim Suchen der angegebenen Speicherdaten aufgetreten: {0}", "FolderDialogExtractTitle": "Wähle den Ordner, in welchen die Dateien entpackt werden sollen", "DialogNcaExtractionMessage": "Extrahiert {0} abschnitt von {1}...", - "DialogNcaExtractionTitle": "Ryujinx - NCA-Abschnitt-Extraktor", + "DialogNcaExtractionTitle": "NCA-Abschnitt-Extraktor", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Extraktion fehlgeschlagen. Der Hauptheader der NCA war in der ausgewählten Datei nicht vorhanden.", "DialogNcaExtractionCheckLogErrorMessage": "Extraktion fehlgeschlagen. Überprüfe die Logs für weitere Informationen.", "DialogNcaExtractionSuccessMessage": "Extraktion erfolgreich abgeschlossen.", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "Update wird entpackt...", "DialogUpdaterRenamingMessage": "Update wird umbenannt...", "DialogUpdaterAddingFilesMessage": "Update wird hinzugefügt...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Update abgeschlossen!", "DialogUpdaterRestartMessage": "Ryujinx jetzt neu starten?", "DialogUpdaterNoInternetMessage": "Es besteht keine Verbindung mit dem Internet!", "DialogUpdaterNoInternetSubMessage": "Bitte vergewissern, dass eine funktionierende Internetverbindung existiert!", "DialogUpdaterDirtyBuildMessage": "Inoffizielle Versionen von Ryujinx können nicht aktualisiert werden", - "DialogUpdaterDirtyBuildSubMessage": "Lade Ryujinx bitte von hier herunter, um eine unterstützte Version zu erhalten: https://https://github.com/GreemDev/Ryujinx/releases/", + "DialogUpdaterDirtyBuildSubMessage": "Lade Ryujinx bitte von hier herunter, um eine unterstützte Version zu erhalten: https://ryujinx.app/download", "DialogRestartRequiredMessage": "Neustart erforderlich", "DialogThemeRestartMessage": "Das Design wurde gespeichert. Ein Neustart ist erforderlich, um das Design anzuwenden.", "DialogThemeRestartSubMessage": "Jetzt neu starten?", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "Dateitypen erfolgreich deinstalliert!", "DialogUninstallFileTypesErrorMessage": "Deinstallation der Dateitypen fehlgeschlagen.", "DialogOpenSettingsWindowLabel": "Fenster-Einstellungen öffnen", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "Controller-Applet", "DialogMessageDialogErrorExceptionMessage": "Fehler bei der Anzeige des Meldungs-Dialogs: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Fehler bei der Anzeige der Software-Tastatur: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nMöchtest du fortfahren?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Firmware wird installiert...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Systemversion {0} wurde erfolgreich installiert.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "Es können keine anderen Profile geöffnet werden, wenn das ausgewählte Profil gelöscht wird.", "DialogUserProfileDeletionConfirmMessage": "Möchtest du das ausgewählte Profil löschen?", "DialogUserProfileUnsavedChangesTitle": "Warnung - Nicht gespeicherte Änderungen", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "Gibt das Spielverzeichnis an, das der Liste hinzuzufügt wird", "AddGameDirTooltip": "Fügt ein neues Spielverzeichnis hinzu", "RemoveGameDirTooltip": "Entfernt das ausgewähltes Spielverzeichnis", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "Verwende ein eigenes Design für die Emulator-Benutzeroberfläche", "CustomThemePathTooltip": "Gibt den Pfad zum Design für die Emulator-Benutzeroberfläche an", "CustomThemeBrowseTooltip": "Ermöglicht die Suche nach einem benutzerdefinierten Design für die Emulator-Benutzeroberfläche", @@ -606,6 +635,8 @@ "DebugLogTooltip": "Ausgabe von Debug-Logs in der Konsole.\n\nVerwende diese Option nur auf ausdrückliche Anweisung von Ryujinx Entwicklern, da sie das Lesen der Protokolle erschwert und die Leistung des Emulators verschlechtert.", "LoadApplicationFileTooltip": "Öffnet die Dateiauswahl um Datei zu laden, welche mit der Switch kompatibel ist", "LoadApplicationFolderTooltip": "Öffnet die Dateiauswahl um ein Spiel zu laden, welches mit der Switch kompatibel ist", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "Öffnet den Ordner, der das Ryujinx Dateisystem enthält", "OpenRyujinxLogsTooltip": "Öffnet den Ordner, in welchem die Logs gespeichert werden", "ExitTooltip": "Beendet Ryujinx", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "Öffne den 'Setup Guide'", "NoUpdate": "Kein Update", "TitleUpdateVersionLabel": "Version {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Bestätigung", "FileDialogAllTypes": "Alle Typen", "Never": "Niemals", "SwkbdMinCharacters": "Muss mindestens {0} Zeichen lang sein", "SwkbdMinRangeCharacters": "Muss {0}-{1} Zeichen lang sein", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Software-Tastatur", "SoftwareKeyboardModeNumeric": "Darf nur 0-9 oder \".\" sein", "SoftwareKeyboardModeAlphabet": "Keine CJK-Zeichen", @@ -709,16 +751,53 @@ "SelectDlcDialogTitle": "DLC-Dateien auswählen", "SelectUpdateDialogTitle": "Update-Datei auswählen", "SelectModDialogTitle": "Mod-Ordner auswählen", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "Benutzerprofile verwalten", "CheatWindowTitle": "Spiel-Cheats verwalten", "DlcWindowTitle": "Spiel-DLC verwalten", "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Spiel-Updates verwalten", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "Cheats verfügbar für {0} [{1}]", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "BuildId": "BuildId:", "DlcWindowHeading": "DLC verfügbar für {0} [{1}]", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Profil bearbeiten", + "Continue": "Continue", "Cancel": "Abbrechen", "Save": "Speichern", "Discard": "Verwerfen", @@ -767,6 +846,7 @@ "GraphicsScalingFilterBilinear": "Bilinear", "GraphicsScalingFilterNearest": "Nächstes", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "Stufe", "GraphicsScalingFilterLevelTooltip": "FSR 1.0 Schärfelevel festlegen. Höher ist schärfer.", "SmaaLow": "SMAA Niedrig", @@ -785,5 +865,17 @@ "MultiplayerMode": "Modus:", "MultiplayerModeTooltip": "Ändert den LDN-Mehrspielermodus.\n\nLdnMitm ändert die lokale drahtlose/lokale Spielfunktionalität in Spielen so, dass sie wie ein LAN funktioniert und lokale, netzwerkgleiche Verbindungen mit anderen Ryujinx-Instanzen und gehackten Nintendo Switch-Konsolen ermöglicht, auf denen das ldn_mitm-Modul installiert ist.\n\nMultiplayer erfordert, dass alle Spieler die gleiche Spielversion verwenden (d.h. Super Smash Bros. Ultimate v13.0.1 kann sich nicht mit v13.0.0 verbinden).\n\nIm Zweifelsfall auf DISABLED lassen.", "MultiplayerModeDisabled": "Deaktiviert", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json index 89389d337..e93e9310a 100644 --- a/src/Ryujinx/Assets/Locales/el_GR.json +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -1,6 +1,7 @@ { "Language": "Ελληνικά", "MenuBarFileOpenApplet": "Άνοιγμα Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Άνοιγμα του Mii Editor Applet σε Αυτόνομη λειτουργία", "SettingsTabInputDirectMouseAccess": "Άμεση Πρόσβαση Ποντικιού", "SettingsTabSystemMemoryManagerMode": "Λειτουργία Διαχείρισης Μνήμης:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "Χρήση Hypervisor", "MenuBarFile": "_Αρχείο", "MenuBarFileOpenFromFile": "_Φόρτωση Αρχείου Εφαρμογής", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "Φόρτωση Απακετάριστου _Παιχνιδιού", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "Άνοιγμα Φακέλου Ryujinx", "MenuBarFileOpenLogsFolder": "Άνοιγμα Φακέλου Καταγραφής", "MenuBarFileExit": "_Έξοδος", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "Εγκατάσταση Firmware", "MenuBarFileToolsInstallFirmwareFromFile": "Εγκατάσταση Firmware από XCI ή ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "Εγκατάσταση Firmware από τοποθεσία", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Διαχείριση τύπων αρχείων", "MenuBarToolsInstallFileTypes": "Εγκαταστήσετε τύπους αρχείων.", "MenuBarToolsUninstallFileTypes": "Απεγκαταστήσετε τύπους αρχείων", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_View", "MenuBarViewWindow": "Window Size", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} Φορτωμένα Παιχνίδια", "StatusBarSystemVersion": "Έκδοση Συστήματος: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Εντοπίστηκε χαμηλό όριο για αντιστοιχίσεις μνήμης", "LinuxVmMaxMapCountDialogTextPrimary": "Θα θέλατε να αυξήσετε την τιμή του vm.max_map_count σε {0}", "LinuxVmMaxMapCountDialogTextSecondary": "Μερικά παιχνίδια μπορεί να προσπαθήσουν να δημιουργήσουν περισσότερες αντιστοιχίσεις μνήμης από αυτές που επιτρέπονται τώρα. Ο Ryujinx θα καταρρεύσει μόλις ξεπεραστεί αυτό το όριο.", @@ -103,6 +114,8 @@ "SettingsTabGeneralHideCursorOnIdle": "Απόκρυψη Δρομέα στην Αδράνεια", "SettingsTabGeneralHideCursorAlways": "Πάντα", "SettingsTabGeneralGameDirectories": "Τοποθεσίες παιχνιδιών", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "Προσθήκη", "SettingsTabGeneralRemove": "Αφαίρεση", "SettingsTabSystem": "Σύστημα", @@ -395,6 +408,8 @@ "InputDialogTitle": "Διάλογος Εισαγωγής", "InputDialogOk": "ΟΚ", "InputDialogCancel": "Ακύρωση", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "Επιλογή Ονόματος Προφίλ", "InputDialogAddNewProfileHeader": "Εισαγωγή Ονόματος Προφίλ", "InputDialogAddNewProfileSubtext": "(Σύνολο Χαρακτήρων: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "Ορισμός Χρώματος Φόντου", "AvatarClose": "Κλείσιμο", "ControllerSettingsLoadProfileToolTip": "Φόρτωση Προφίλ", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Προσθήκη Προφίλ", "ControllerSettingsRemoveProfileToolTip": "Κατάργηση Προφίλ", "ControllerSettingsSaveProfileToolTip": "Αποθήκευση Προφίλ", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "Εναλλαγή Αγαπημένου", "GameListContextMenuToggleFavoriteToolTip": "Εναλλαγή της Κατάστασης Αγαπημένο του Παιχνιδιού", "SettingsTabGeneralTheme": "Theme:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "Dark", "SettingsTabGeneralThemeLight": "Light", "ControllerSettingsConfigureGeneral": "Παραμέτρων", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "Σφάλμα κατά την εύρεση των αποθηκευμένων δεδομένων: {0}", "FolderDialogExtractTitle": "Επιλέξτε τον φάκελο στον οποίο θέλετε να εξαγάγετε", "DialogNcaExtractionMessage": "Εξαγωγή ενότητας {0} από {1}...", - "DialogNcaExtractionTitle": "Ryujinx - NCA Εξαγωγέας Τμημάτων", + "DialogNcaExtractionTitle": "NCA Εξαγωγέας Τμημάτων", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Αποτυχία εξαγωγής. Η κύρια NCA δεν υπήρχε στο επιλεγμένο αρχείο.", "DialogNcaExtractionCheckLogErrorMessage": "Αποτυχία εξαγωγής. Διαβάστε το αρχείο καταγραφής για περισσότερες πληροφορίες.", "DialogNcaExtractionSuccessMessage": "Η εξαγωγή ολοκληρώθηκε με επιτυχία.", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "Εξαγωγή Ενημέρωσης...", "DialogUpdaterRenamingMessage": "Μετονομασία Ενημέρωσης...", "DialogUpdaterAddingFilesMessage": "Προσθήκη Νέας Ενημέρωσης...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Η Ενημέρωση Ολοκληρώθηκε!", "DialogUpdaterRestartMessage": "Θέλετε να επανεκκινήσετε το Ryujinx τώρα;", "DialogUpdaterNoInternetMessage": "Δεν είστε συνδεδεμένοι στο Διαδίκτυο!", "DialogUpdaterNoInternetSubMessage": "Επαληθεύστε ότι έχετε σύνδεση στο Διαδίκτυο που λειτουργεί!", "DialogUpdaterDirtyBuildMessage": "Δεν μπορείτε να ενημερώσετε μία Πρόχειρη Έκδοση του Ryujinx!", - "DialogUpdaterDirtyBuildSubMessage": "Κάντε λήψη του Ryujinx στη διεύθυνση https://https://github.com/GreemDev/Ryujinx/releases/ εάν αναζητάτε μία υποστηριζόμενη έκδοση.", + "DialogUpdaterDirtyBuildSubMessage": "Κάντε λήψη του Ryujinx στη διεύθυνση https://ryujinx.app/download εάν αναζητάτε μία υποστηριζόμενη έκδοση.", "DialogRestartRequiredMessage": "Απαιτείται Επανεκκίνηση", "DialogThemeRestartMessage": "Το θέμα έχει αποθηκευτεί. Απαιτείται επανεκκίνηση για την εφαρμογή του θέματος.", "DialogThemeRestartSubMessage": "Θέλετε να κάνετε επανεκκίνηση", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "Επιτυχής απεγκατάσταση τύπων αρχείων!", "DialogUninstallFileTypesErrorMessage": "Αποτυχία απεγκατάστασης τύπων αρχείων.", "DialogOpenSettingsWindowLabel": "Άνοιγμα Παραθύρου Ρυθμίσεων", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "Applet Χειρισμού", "DialogMessageDialogErrorExceptionMessage": "Σφάλμα εμφάνισης του διαλόγου Μηνυμάτων: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Σφάλμα εμφάνισης Λογισμικού Πληκτρολογίου: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nΘέλετε να συνεχίσετε;", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Εγκατάσταση Firmware...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Η έκδοση συστήματος {0} εγκαταστάθηκε με επιτυχία.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "Δεν θα υπάρχουν άλλα προφίλ εάν διαγραφεί το επιλεγμένο", "DialogUserProfileDeletionConfirmMessage": "Θέλετε να διαγράψετε το επιλεγμένο προφίλ", "DialogUserProfileUnsavedChangesTitle": "Προσοχή - Μην Αποθηκευμένες Αλλαγές.", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "Εισαγάγετε μία τοποθεσία παιχνιδιών για προσθήκη στη λίστα", "AddGameDirTooltip": "Προσθέστε μία τοποθεσία παιχνιδιών στη λίστα", "RemoveGameDirTooltip": "Αφαιρέστε την επιλεγμένη τοποθεσία παιχνιδιών", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "Ενεργοποίηση ή απενεργοποίηση προσαρμοσμένων θεμάτων στο GUI", "CustomThemePathTooltip": "Διαδρομή προς το προσαρμοσμένο θέμα GUI", "CustomThemeBrowseTooltip": "Αναζητήστε ένα προσαρμοσμένο θέμα GUI", @@ -606,6 +635,8 @@ "DebugLogTooltip": "Ενεργοποιεί την εκτύπωση μηνυμάτων αρχείου καταγραφής εντοπισμού σφαλμάτων", "LoadApplicationFileTooltip": "Ανοίξτε έναν επιλογέα αρχείων για να επιλέξετε ένα αρχείο συμβατό με το Switch για φόρτωση", "LoadApplicationFolderTooltip": "Ανοίξτε έναν επιλογέα αρχείων για να επιλέξετε μία μη συσκευασμένη εφαρμογή, συμβατή με το Switch για φόρτωση", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "Ανοίξτε το φάκελο συστήματος αρχείων Ryujinx", "OpenRyujinxLogsTooltip": "Ανοίξτε το φάκελο στον οποίο διατηρούνται τα αρχεία καταγραφής", "ExitTooltip": "Έξοδος από το Ryujinx", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "Ανοίξτε τον Οδηγό Εγκατάστασης.", "NoUpdate": "Καμία Eνημέρωση", "TitleUpdateVersionLabel": "Version {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - Πληροφορίες", "RyujinxConfirm": "Ryujinx - Επιβεβαίωση", "FileDialogAllTypes": "Όλοι οι τύποι", "Never": "Ποτέ", "SwkbdMinCharacters": "Πρέπει να έχει μήκος τουλάχιστον {0} χαρακτήρες", "SwkbdMinRangeCharacters": "Πρέπει να έχει μήκος {0}-{1} χαρακτήρες", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Εικονικό Πληκτρολόγιο", "SoftwareKeyboardModeNumeric": "Πρέπει να είναι 0-9 ή '.' μόνο", "SoftwareKeyboardModeAlphabet": "Πρέπει να μην είναι μόνο χαρακτήρες CJK", @@ -709,16 +751,53 @@ "SelectDlcDialogTitle": "Επιλογή αρχείων DLC", "SelectUpdateDialogTitle": "Επιλογή αρχείων ενημέρωσης", "SelectModDialogTitle": "Select mod directory", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "Διαχειριστής Προφίλ Χρήστη", "CheatWindowTitle": "Διαχειριστής των Cheats", "DlcWindowTitle": "Downloadable Content Manager", "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Διαχειριστής Ενημερώσεων Τίτλου", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "Διαθέσιμα Cheats για {0} [{1}]", "BuildId": "BuildId:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Επεξεργασία Επιλεγμένων", + "Continue": "Continue", "Cancel": "Ακύρωση", "Save": "Αποθήκευση", "Discard": "Απόρριψη", @@ -767,6 +846,7 @@ "GraphicsScalingFilterBilinear": "Bilinear", "GraphicsScalingFilterNearest": "Nearest", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "Επίπεδο", "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.", "SmaaLow": "Χαμηλό SMAA", @@ -785,5 +865,17 @@ "MultiplayerMode": "Λειτουργία:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", "MultiplayerModeDisabled": "Disabled", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 68b48146b..ee0d03171 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -1,6 +1,7 @@ { "Language": "English (US)", "MenuBarFileOpenApplet": "Open Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Open Mii Editor Applet in Standalone mode", "SettingsTabInputDirectMouseAccess": "Direct Mouse Access", "SettingsTabSystemMemoryManagerMode": "Memory Manager Mode:", @@ -30,9 +31,13 @@ "MenuBarToolsInstallFirmware": "Install Firmware", "MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "Install a firmware from a directory", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Manage file types", "MenuBarToolsInstallFileTypes": "Install file types", "MenuBarToolsUninstallFileTypes": "Uninstall file types", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_View", "MenuBarViewWindow": "Window Size", "MenuBarViewWindow720": "720p", @@ -84,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} Games Loaded", "StatusBarSystemVersion": "System Version: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Low limit for memory mappings detected", "LinuxVmMaxMapCountDialogTextPrimary": "Would you like to increase the value of vm.max_map_count to {0}", "LinuxVmMaxMapCountDialogTextSecondary": "Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.", @@ -138,9 +146,20 @@ "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Latin American Spanish", "SettingsTabSystemSystemLanguageSimplifiedChinese": "Simplified Chinese", "SettingsTabSystemSystemLanguageTraditionalChinese": "Traditional Chinese", - "SettingsTabSystemSystemTimeZone": "System TimeZone:", + "SettingsTabSystemSystemTimeZone": "System Time Zone:", "SettingsTabSystemSystemTime": "System Time:", - "SettingsTabSystemEnableVsync": "VSync", + "SettingsTabSystemVSyncMode": "VSync:", + "SettingsTabSystemEnableCustomVSyncInterval": "Enable custom refresh rate (Experimental)", + "SettingsTabSystemVSyncModeSwitch": "Switch", + "SettingsTabSystemVSyncModeUnbounded": "Unbounded", + "SettingsTabSystemVSyncModeCustom": "Custom Refresh Rate", + "SettingsTabSystemVSyncModeTooltip": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate.", + "SettingsTabSystemVSyncModeTooltipCustom": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate. 'Custom' emulates the specified custom refresh rate.", + "SettingsTabSystemEnableCustomVSyncIntervalTooltip": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.", + "SettingsTabSystemCustomVSyncIntervalValueTooltip": "The custom refresh rate target value.", + "SettingsTabSystemCustomVSyncIntervalSliderTooltip": "The custom refresh rate, as a percentage of the normal Switch refresh rate.", + "SettingsTabSystemCustomVSyncIntervalPercentage": "Custom Refresh Rate %:", + "SettingsTabSystemCustomVSyncIntervalValue": "Custom Refresh Rate Value:", "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)", "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC cache", "SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks", @@ -149,6 +168,7 @@ "SettingsTabSystemAudioBackendOpenAL": "OpenAL", "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", + "SettingsTabSystemCustomVSyncInterval": "Interval", "SettingsTabSystemHacks": "Hacks", "SettingsTabSystemHacksNote": "May cause instability", "SettingsTabSystemDramSize": "DRAM size:", @@ -400,6 +420,8 @@ "InputDialogTitle": "Input Dialog", "InputDialogOk": "OK", "InputDialogCancel": "Cancel", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "Choose the Profile Name", "InputDialogAddNewProfileHeader": "Please Enter a Profile Name", "InputDialogAddNewProfileSubtext": "(Max Length: {0})", @@ -407,6 +429,7 @@ "AvatarSetBackgroundColor": "Set Background Color", "AvatarClose": "Close", "ControllerSettingsLoadProfileToolTip": "Load Profile", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Add Profile", "ControllerSettingsRemoveProfileToolTip": "Remove Profile", "ControllerSettingsSaveProfileToolTip": "Save Profile", @@ -437,25 +460,26 @@ "DialogMessageFindSaveErrorMessage": "There was an error finding the specified savedata: {0}", "FolderDialogExtractTitle": "Choose the folder to extract into", "DialogNcaExtractionMessage": "Extracting {0} section from {1}...", - "DialogNcaExtractionTitle": "Ryujinx - NCA Section Extractor", + "DialogNcaExtractionTitle": "NCA Section Extractor", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Extraction failure. The main NCA was not present in the selected file.", - "DialogNcaExtractionCheckLogErrorMessage": "Extraction failure. Read the log file for further information.", + "DialogNcaExtractionCheckLogErrorMessage": "Extraction failed. Please check the log file for more details.", "DialogNcaExtractionSuccessMessage": "Extraction completed successfully.", - "DialogUpdaterConvertFailedMessage": "Failed to convert the current Ryujinx version.", - "DialogUpdaterCancelUpdateMessage": "Cancelling Update!", - "DialogUpdaterAlreadyOnLatestVersionMessage": "You are already using the most updated version of Ryujinx!", - "DialogUpdaterFailedToGetVersionMessage": "An error has occurred when trying to get release information from GitHub Release. This can be caused if a new release is being compiled by GitHub Actions. Try again in a few minutes.", - "DialogUpdaterConvertFailedGithubMessage": "Failed to convert the received Ryujinx version from Github Release.", + "DialogUpdaterConvertFailedMessage": "Unable to convert the current Ryujinx version.", + "DialogUpdaterCancelUpdateMessage": "Update canceled!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "You are already using the latest version of Ryujinx!", + "DialogUpdaterFailedToGetVersionMessage": "An error occurred while trying to retrieve release information from GitHub. This may happen if a new release is currently being compiled by GitHub Actions. Please try again in a few minutes.", + "DialogUpdaterConvertFailedGithubMessage": "Failed to convert the Ryujinx version received from GitHub.", "DialogUpdaterDownloadingMessage": "Downloading Update...", "DialogUpdaterExtractionMessage": "Extracting Update...", "DialogUpdaterRenamingMessage": "Renaming Update...", "DialogUpdaterAddingFilesMessage": "Adding New Update...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Update Complete!", "DialogUpdaterRestartMessage": "Do you want to restart Ryujinx now?", "DialogUpdaterNoInternetMessage": "You are not connected to the Internet!", "DialogUpdaterNoInternetSubMessage": "Please verify that you have a working Internet connection!", - "DialogUpdaterDirtyBuildMessage": "You Cannot update a Dirty build of Ryujinx!", - "DialogUpdaterDirtyBuildSubMessage": "Please download Ryujinx at https://https://github.com/GreemDev/Ryujinx/releases/ if you are looking for a supported version.", + "DialogUpdaterDirtyBuildMessage": "You cannot update a Dirty build of Ryujinx!", + "DialogUpdaterDirtyBuildSubMessage": "Please download Ryujinx at https://ryujinx.app/download if you are looking for a supported version.", "DialogRestartRequiredMessage": "Restart Required", "DialogThemeRestartMessage": "Theme has been saved. A restart is needed to apply the theme.", "DialogThemeRestartSubMessage": "Do you want to restart", @@ -468,6 +492,7 @@ "DialogUninstallFileTypesSuccessMessage": "Successfully uninstalled file types!", "DialogUninstallFileTypesErrorMessage": "Failed to uninstall file types.", "DialogOpenSettingsWindowLabel": "Open Settings Window", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "Controller Applet", "DialogMessageDialogErrorExceptionMessage": "Error displaying Message Dialog: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Error displaying Software Keyboard: {0}", @@ -496,6 +521,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDo you want to continue?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installing firmware...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted", "DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile", "DialogUserProfileUnsavedChangesTitle": "Warning - Unsaved Changes", @@ -670,12 +702,21 @@ "TitleUpdateVersionLabel": "Version {0}", "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Confirmation", "FileDialogAllTypes": "All types", "Never": "Never", "SwkbdMinCharacters": "Must be at least {0} characters long", "SwkbdMinRangeCharacters": "Must be {0}-{1} characters long", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Software Keyboard", "SoftwareKeyboardModeNumeric": "Must be 0-9 or '.' only", "SoftwareKeyboardModeAlphabet": "Must be non CJK-characters only", @@ -705,11 +746,13 @@ "RyujinxUpdater": "Ryujinx Updater", "SettingsTabHotkeys": "Keyboard Hotkeys", "SettingsTabHotkeysHotkeys": "Keyboard Hotkeys", - "SettingsTabHotkeysToggleVsyncHotkey": "Toggle VSync:", + "SettingsTabHotkeysToggleVSyncModeHotkey": "Toggle VSync mode:", "SettingsTabHotkeysScreenshotHotkey": "Screenshot:", "SettingsTabHotkeysShowUiHotkey": "Show UI:", "SettingsTabHotkeysPauseHotkey": "Pause:", "SettingsTabHotkeysToggleMuteHotkey": "Mute:", + "SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey": "Raise custom refresh rate", + "SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey": "Lower custom refresh rate", "ControllerMotionTitle": "Motion Control Settings", "ControllerRumbleTitle": "Rumble Settings", "SettingsSelectThemeFileDialogTitle": "Select Theme File", @@ -722,17 +765,45 @@ "SelectDlcDialogTitle": "Select DLC files", "SelectUpdateDialogTitle": "Select update files", "SelectModDialogTitle": "Select mod directory", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "User Profiles Manager", "CheatWindowTitle": "Cheats Manager", "DlcWindowTitle": "Manage Downloadable Content for {0} ({1})", "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Title Update Manager", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "Cheats Available for {0} [{1}]", "BuildId": "BuildId:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", - "DlcWindowHeading": "{0} Downloadable Content(s)", + "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", @@ -740,6 +811,7 @@ "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Edit Selected", + "Continue": "Continue", "Cancel": "Cancel", "Save": "Save", "Discard": "Discard", @@ -807,5 +879,17 @@ "MultiplayerMode": "Mode:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", "MultiplayerModeDisabled": "Disabled", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index d6eb8017a..0a68d44c6 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -1,6 +1,7 @@ { "Language": "Español (ES)", "MenuBarFileOpenApplet": "Abrir applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Abre el editor de Mii en modo autónomo", "SettingsTabInputDirectMouseAccess": "Acceso directo al ratón", "SettingsTabSystemMemoryManagerMode": "Modo del administrador de memoria:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "Usar hipervisor", "MenuBarFile": "_Archivo", "MenuBarFileOpenFromFile": "_Cargar aplicación desde un archivo", + "MenuBarFileOpenFromFileError": "No se encontraron aplicaciones en el archivo seleccionado.", "MenuBarFileOpenUnpacked": "Cargar juego _desempaquetado", + "MenuBarFileLoadDlcFromFolder": "Cargar DLC Desde Carpeta", + "MenuBarFileLoadTitleUpdatesFromFolder": "Cargar Actualizaciones de Títulos Desde Carpeta", "MenuBarFileOpenEmuFolder": "Abrir carpeta de Ryujinx", "MenuBarFileOpenLogsFolder": "Abrir carpeta de registros", "MenuBarFileExit": "_Salir", @@ -27,11 +31,15 @@ "MenuBarToolsInstallFirmware": "Instalar firmware", "MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware desde un archivo XCI o ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "Instalar firmware desde una carpeta", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Administrar tipos de archivo", "MenuBarToolsInstallFileTypes": "Instalar tipos de archivo", "MenuBarToolsUninstallFileTypes": "Desinstalar tipos de archivo", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_View", - "MenuBarViewWindow": "Window Size", + "MenuBarViewWindow": "Tamaño Ventana", "MenuBarViewWindow720": "720p", "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Ayuda", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Abre el directorio que contiene los Mods de la Aplicación.", "GameListContextMenuOpenSdModsDirectory": "Abrir Directorio de Mods de Atmosphere\n\n\n\n\n\n", "GameListContextMenuOpenSdModsDirectoryToolTip": "Abre el directorio alternativo de la tarjeta SD de Atmosphere que contiene los Mods de la Aplicación. Útil para los mods que están empaquetados para el hardware real.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} juegos cargados", "StatusBarSystemVersion": "Versión del sistema: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Límite inferior para mapeos de memoria detectado", "LinuxVmMaxMapCountDialogTextPrimary": "¿Quieres aumentar el valor de vm.max_map_count a {0}?", "LinuxVmMaxMapCountDialogTextSecondary": "Algunos juegos podrían intentar crear más mapeos de memoria de los permitidos. Ryujinx se bloqueará tan pronto como se supere este límite.", @@ -96,13 +107,15 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Habilitar estado en Discord", "SettingsTabGeneralCheckUpdatesOnLaunch": "Buscar actualizaciones al iniciar", "SettingsTabGeneralShowConfirmExitDialog": "Mostrar diálogo de confirmación al cerrar", - "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", - "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralRememberWindowState": "Recordar Tamaño/Posición de la Ventana", + "SettingsTabGeneralShowTitleBar": "Mostrar Barra de Título (Requiere reinicio)", "SettingsTabGeneralHideCursor": "Esconder el cursor:", "SettingsTabGeneralHideCursorNever": "Nunca", "SettingsTabGeneralHideCursorOnIdle": "Ocultar cursor cuando esté inactivo", "SettingsTabGeneralHideCursorAlways": "Siempre", "SettingsTabGeneralGameDirectories": "Carpetas de juegos", + "SettingsTabGeneralAutoloadDirectories": "Carpetas de DLC/Actualizaciones para Carga Automática", + "SettingsTabGeneralAutoloadNote": "DLC y Actualizaciones que hacen referencia a archivos ausentes serán desactivado automáticamente", "SettingsTabGeneralAdd": "Agregar", "SettingsTabGeneralRemove": "Quitar", "SettingsTabSystem": "Sistema", @@ -137,10 +150,10 @@ "SettingsTabSystemSystemTime": "Hora del sistema:", "SettingsTabSystemEnableVsync": "Sincronización vertical", "SettingsTabSystemEnablePptc": "PPTC (Cache de Traducción de Perfil Persistente)", - "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableLowPowerPptc": "Cache PPTC de bajo consumo", "SettingsTabSystemEnableFsIntegrityChecks": "Comprobar integridad de los archivos", "SettingsTabSystemAudioBackend": "Motor de audio:", - "SettingsTabSystemAudioBackendDummy": "Vacio", + "SettingsTabSystemAudioBackendDummy": "Vacío", "SettingsTabSystemAudioBackendOpenAL": "OpenAL", "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", @@ -395,6 +408,8 @@ "InputDialogTitle": "Cuadro de diálogo de entrada", "InputDialogOk": "Aceptar", "InputDialogCancel": "Cancelar", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "Introducir nombre de perfil", "InputDialogAddNewProfileHeader": "Por favor elige un nombre de usuario", "InputDialogAddNewProfileSubtext": "(Máximo de caracteres: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "Establecer color de fondo", "AvatarClose": "Cerrar", "ControllerSettingsLoadProfileToolTip": "Cargar perfil", + "ControllerSettingsViewProfileToolTip": "Ver perfil", "ControllerSettingsAddProfileToolTip": "Agregar perfil", "ControllerSettingsRemoveProfileToolTip": "Eliminar perfil", "ControllerSettingsSaveProfileToolTip": "Guardar perfil", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "Marcar favorito", "GameListContextMenuToggleFavoriteToolTip": "Marca o desmarca el juego como favorito", "SettingsTabGeneralTheme": "Tema:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "Oscuro", "SettingsTabGeneralThemeLight": "Claro", "ControllerSettingsConfigureGeneral": "Configurar", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "Hubo un error encontrando los datos de guardado especificados: {0}", "FolderDialogExtractTitle": "Elige la carpeta en la que deseas extraer", "DialogNcaExtractionMessage": "Extrayendo {0} sección de {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Extractor de sección NCA", + "DialogNcaExtractionTitle": "Extractor de sección NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Fallo de extracción. El NCA principal no estaba presente en el archivo seleccionado.", "DialogNcaExtractionCheckLogErrorMessage": "Fallo de extracción. Lee el registro para más información.", "DialogNcaExtractionSuccessMessage": "Se completó la extracción con éxito.", @@ -444,17 +461,18 @@ "DialogUpdaterExtractionMessage": "Extrayendo actualización...", "DialogUpdaterRenamingMessage": "Renombrando actualización...", "DialogUpdaterAddingFilesMessage": "Aplicando actualización...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "¡Actualización completa!", "DialogUpdaterRestartMessage": "¿Quieres reiniciar Ryujinx?", "DialogUpdaterNoInternetMessage": "¡No estás conectado a internet!", "DialogUpdaterNoInternetSubMessage": "¡Por favor, verifica que tu conexión a Internet funciona!", "DialogUpdaterDirtyBuildMessage": "¡No puedes actualizar una versión \"dirty\" de Ryujinx!", - "DialogUpdaterDirtyBuildSubMessage": "Por favor, descarga Ryujinx en https://https://github.com/GreemDev/Ryujinx/releases/ si buscas una versión con soporte.", + "DialogUpdaterDirtyBuildSubMessage": "Por favor, descarga Ryujinx en https://ryujinx.app/download si buscas una versión con soporte.", "DialogRestartRequiredMessage": "Se necesita reiniciar", "DialogThemeRestartMessage": "Tema guardado. Se necesita reiniciar para aplicar el tema.", "DialogThemeRestartSubMessage": "¿Quieres reiniciar?", "DialogFirmwareInstallEmbeddedMessage": "¿Quieres instalar el firmware incluido en este juego? (Firmware versión {0})", - "DialogFirmwareInstallEmbeddedSuccessMessage": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\nThe emulator will now start.", + "DialogFirmwareInstallEmbeddedSuccessMessage": "No se encontró ning{un firmware instalado pero Ryujinx pudo instalar firmware {0} del juego proporcionado.\nEl emulador iniciará.", "DialogFirmwareNoFirmwareInstalledMessage": "No hay firmware instalado", "DialogFirmwareInstalledMessage": "Se instaló el firmware {0}", "DialogInstallFileTypesSuccessMessage": "¡Tipos de archivos instalados con éxito!", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "¡Tipos de archivos desinstalados con éxito!", "DialogUninstallFileTypesErrorMessage": "No se pudo desinstalar los tipos de archivo.", "DialogOpenSettingsWindowLabel": "Abrir ventana de opciones", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "Applet de mandos", "DialogMessageDialogErrorExceptionMessage": "Error al mostrar cuadro de diálogo: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Error al mostrar teclado de software: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n¿Continuar?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalando firmware...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Versión de sistema {0} instalada con éxito.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "Si eliminas el perfil seleccionado no quedará ningún otro perfil", "DialogUserProfileDeletionConfirmMessage": "¿Quieres eliminar el perfil seleccionado?", "DialogUserProfileUnsavedChangesTitle": "Advertencia - Cambios sin guardar", @@ -561,19 +587,22 @@ "AddGameDirBoxTooltip": "Elige un directorio de juegos para mostrar en la ventana principal", "AddGameDirTooltip": "Agrega un directorio de juegos a la lista", "RemoveGameDirTooltip": "Quita el directorio seleccionado de la lista", + "AddAutoloadDirBoxTooltip": "Elige un directorio de carga automática para agregar a la lista", + "AddAutoloadDirTooltip": "Agregar un directorio de carga automática a la lista", + "RemoveAutoloadDirTooltip": "Eliminar el directorio de carga automática seleccionado", "CustomThemeCheckTooltip": "Activa o desactiva los temas personalizados para la interfaz", "CustomThemePathTooltip": "Carpeta que contiene los temas personalizados para la interfaz", "CustomThemeBrowseTooltip": "Busca un tema personalizado para la interfaz", "DockModeToggleTooltip": "El modo dock o modo TV hace que la consola emulada se comporte como una Nintendo Switch en su dock. Esto mejora la calidad gráfica en la mayoría de los juegos. Del mismo modo, si lo desactivas, el sistema emulado se comportará como una Nintendo Switch en modo portátil, reduciendo la cálidad de los gráficos.\n\nConfigura los controles de \"Jugador\" 1 si planeas jugar en modo dock/TV; configura los controles de \"Portátil\" si planeas jugar en modo portátil.\n\nActívalo si no sabes qué hacer.", - "DirectKeyboardTooltip": "Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.\n\nOnly works with games that natively support keyboard usage on Switch hardware.\n\nLeave OFF if unsure.", - "DirectMouseTooltip": "Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.\n\nOnly works with games that natively support mouse controls on Switch hardware, which are few and far between.\n\nWhen enabled, touch screen functionality may not work.\n\nLeave OFF if unsure.", + "DirectKeyboardTooltip": "Soporte de acceso directo al teclado (HID). Proporciona a los juegos acceso a su teclado como dispositivo de entrada de texto.\n\nSolo funciona con juegos que permiten de forma nativa el uso del teclado en el hardware de Switch.\n\nDesactívalo si no sabes qué hacer.", + "DirectMouseTooltip": "Soporte de acceso directo al mouse (HID). Proporciona a los juegos acceso a su mouse como puntero.\n\nSolo funciona con juegos que permiten de forma nativa el uso de controles con mouse en el hardware de switch, lo cual son pocos.\n\nCuando esté activado, la funcionalidad de pantalla táctil puede no funcionar.\n\nDesactívalo si no sabes qué hacer.", "RegionTooltip": "Cambia la región del sistema", "LanguageTooltip": "Cambia el idioma del sistema", "TimezoneTooltip": "Cambia la zona horaria del sistema", "TimeTooltip": "Cambia la hora del sistema", - "VSyncToggleTooltip": "Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck.\n\nCan be toggled in-game with a hotkey of your preference (F1 by default). We recommend doing this if you plan on disabling it.\n\nLeave ON if unsure.", + "VSyncToggleTooltip": "Sincronización vertical de la consola emulada. En práctica un limitador del framerate para la mayoría de los juegos; desactivando puede causar que juegos corran a mayor velocidad o que las pantallas de carga tarden más o queden atascados.\n\nSe puede alternar en juego utilizando una tecla de acceso rápido configurable (F1 by default). Recomendamos hacer esto en caso de querer desactivar sincroniziación vertical.\n\nDesactívalo si no sabes qué hacer.", "PptcToggleTooltip": "Guarda funciones de JIT traducidas para que no sea necesario traducirlas cada vez que el juego carga.\n\nReduce los tirones y acelera significativamente el tiempo de inicio de los juegos después de haberlos ejecutado al menos una vez.\n\nActívalo si no sabes qué hacer.", - "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "LowPowerPptcToggleTooltip": "Cargue el PPTC utilizando un tercio de la cantidad de núcleos.", "FsIntegrityToggleTooltip": "Comprueba si hay archivos corruptos en los juegos que ejecutes al abrirlos, y si detecta archivos corruptos, muestra un error de Hash en los registros.\n\nEsto no tiene impacto alguno en el rendimiento y está pensado para ayudar a resolver problemas.\n\nActívalo si no sabes qué hacer.", "AudioBackendTooltip": "Cambia el motor usado para renderizar audio.\n\nSDL2 es el preferido, mientras que OpenAL y SoundIO se usan si hay problemas con este. Dummy no produce audio.\n\nSelecciona SDL2 si no sabes qué hacer.", "MemoryManagerTooltip": "Cambia la forma de mapear y acceder a la memoria del guest. Afecta en gran medida al rendimiento de la CPU emulada.\n\nSelecciona \"Host sin verificación\" si no sabes qué hacer.", @@ -587,10 +616,10 @@ "GraphicsBackendThreadingTooltip": "Ejecuta los comandos del motor gráfico en un segundo hilo. Acelera la compilación de sombreadores, reduce los tirones, y mejora el rendimiento en controladores gráficos que no realicen su propio procesamiento con múltiples hilos. Rendimiento ligeramente superior en controladores gráficos que soporten múltiples hilos.\n\nSelecciona \"Auto\" si no sabes qué hacer.", "GalThreadingTooltip": "Ejecuta los comandos del motor gráfico en un segundo hilo. Acelera la compilación de sombreadores, reduce los tirones, y mejora el rendimiento en controladores gráficos que no realicen su propio procesamiento con múltiples hilos. Rendimiento ligeramente superior en controladores gráficos que soporten múltiples hilos.\n\nSelecciona \"Auto\" si no sabes qué hacer.", "ShaderCacheToggleTooltip": "Guarda una caché de sombreadores en disco, la cual reduce los tirones a medida que vas jugando.\n\nActívalo si no sabes qué hacer.", - "ResolutionScaleTooltip": "Multiplies the game's rendering resolution.\n\nA few games may not work with this and look pixelated even when the resolution is increased; for those games, you may need to find mods that remove anti-aliasing or that increase their internal rendering resolution. For using the latter, you'll likely want to select Native.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nKeep in mind 4x is overkill for virtually any setup.", + "ResolutionScaleTooltip": "Multiplica la resolución de rendereo del juego.\n\nAlgunos juegos podrían no funcionar con esto y verse pixelado al aumentar la resolución; en esos casos, quizás sería necesario buscar mods que de anti-aliasing o que aumenten la resolución interna. Para usar este último, probablemente necesitarás seleccionar Nativa.\n\nEsta opción puede ser modificada mientras que un juego este corriendo haciendo click en \"Aplicar\" más abajo; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nTener en cuenta que 4x es excesivo para prácticamente cualquier configuración.", "ResolutionScaleEntryTooltip": "Escalado de resolución de coma flotante, como por ejemplo 1,5. Los valores no íntegros pueden causar errores gráficos o crashes.", - "AnisotropyTooltip": "Level of Anisotropic Filtering. Set to Auto to use the value requested by the game.", - "AspectRatioTooltip": "Aspect Ratio applied to the renderer window.\n\nOnly change this if you're using an aspect ratio mod for your game, otherwise the graphics will be stretched.\n\nLeave on 16:9 if unsure.", + "AnisotropyTooltip": "Nivel de filtrado anisotrópico. Setear en Auto para utilizar el valor solicitado por el juego.", + "AspectRatioTooltip": "Relación de aspecto aplicada a la ventana del renderizador.\n\nSolamente modificar esto si estás utilizando un mod de relación de aspecto para su juego, en cualquier otro caso los gráficos se estirarán.\n\nDejar en 16:9 si no sabe que hacer.", "ShaderDumpPathTooltip": "Directorio en el cual se volcarán los sombreadores de los gráficos", "FileLogTooltip": "Guarda los registros de la consola en archivos en disco. No afectan al rendimiento.", "StubLogTooltip": "Escribe mensajes de Stub en la consola. No afectan al rendimiento.", @@ -606,6 +635,8 @@ "DebugLogTooltip": "Escribe mensajes de debug en la consola\n\nActiva esto solo si un miembro del equipo te lo pide expresamente, pues hará que el registro sea difícil de leer y empeorará el rendimiento del emulador.", "LoadApplicationFileTooltip": "Abre el explorador de archivos para elegir un archivo compatible con Switch para cargar", "LoadApplicationFolderTooltip": "Abre el explorador de archivos para elegir un archivo desempaquetado y compatible con Switch para cargar", + "LoadDlcFromFolderTooltip": "Abrir un explorador de archivos para seleccionar una o más carpetas para cargar DLC de forma masiva", + "LoadTitleUpdatesFromFolderTooltip": "Abrir un explorador de archivos para seleccionar una o más carpetas para cargar actualizaciones de título de forma masiva", "OpenRyujinxFolderTooltip": "Abre la carpeta de sistema de Ryujinx", "OpenRyujinxLogsTooltip": "Abre la carpeta en la que se guardan los registros", "ExitTooltip": "Cierra Ryujinx", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "Abrir la guía de instalación", "NoUpdate": "No actualizado", "TitleUpdateVersionLabel": "Versión {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Confirmación", "FileDialogAllTypes": "Todos los tipos", "Never": "Nunca", "SwkbdMinCharacters": "Debe tener al menos {0} caracteres", "SwkbdMinRangeCharacters": "Debe tener {0}-{1} caracteres", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Teclado de software", "SoftwareKeyboardModeNumeric": "Debe ser sólo 0-9 o '.'", "SoftwareKeyboardModeAlphabet": "Solo deben ser caracteres no CJK", @@ -709,16 +751,52 @@ "SelectDlcDialogTitle": "Selecciona archivo(s) de DLC", "SelectUpdateDialogTitle": "Selecciona archivo(s) de actualización", "SelectModDialogTitle": "Seleccionar un directorio de Mods", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "Administrar perfiles de usuario", "CheatWindowTitle": "Administrar cheats", "DlcWindowTitle": "Administrar contenido descargable", - "ModWindowTitle": "Manage Mods for {0} ({1})", + "ModWindowTitle": "Administrar Mods para {0} ({1})", "UpdateWindowTitle": "Administrar actualizaciones", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} nueva(s) actualización(es) agregada(s)", + "UpdateWindowBundledContentNotice": "Las actualizaciones agrupadas no pueden ser eliminadas, solamente deshabilitadas.", "CheatWindowHeading": "Cheats disponibles para {0} [{1}]", "BuildId": "Id de compilación:", "DlcWindowHeading": "Contenido descargable disponible para {0} [{1}]", + "DlcWindowDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)", + "AutoloadDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)", + "AutoloadDlcRemovedMessage": "Se eliminaron {0} contenido(s) descargable(s) faltantes", + "AutoloadUpdateAddedMessage": "Se agregaron {0} nueva(s) actualización(es)", + "AutoloadUpdateRemovedMessage": "Se eliminaron {0} actualización(es) faltantes", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Editar selección", + "Continue": "Continue", "Cancel": "Cancelar", "Save": "Guardar", "Discard": "Descartar", @@ -732,9 +810,9 @@ "UserProfilesName": "Nombre:", "UserProfilesUserId": "Id de Usuario:", "SettingsTabGraphicsBackend": "Fondo de gráficos", - "SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.", + "SettingsTabGraphicsBackendTooltip": "Seleccione el backend gráfico que utilizará el emulador.\n\nVulkan, en general, es mejor para todas las tarjetas gráficas modernas, mientras que sus controladores estén actualizados. Vulkan también cuenta con complicación más rápida de sombreadores (menos tirones) en todos los proveredores de GPU.\n\nOpenGL puede lograr mejores resultados en GPU Nvidia antiguas, GPU AMD antiguas en Linux o en GPUs con menor VRAM, aunque tirones de compilación de sombreadores serán mayores.\n\nSetear en Vulkan si no sabe que hacer. Setear en OpenGL si su GPU no tiene soporte para Vulkan aún con los últimos controladores gráficos.", "SettingsEnableTextureRecompression": "Activar recompresión de texturas", - "SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.", + "SettingsEnableTextureRecompressionTooltip": "Comprimir texturas ASTC para reducir uso de VRAM.\n\nJuegos que utilizan este formato de textura incluyen Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder y The Legend of Zelda: Tears of the Kingdom.\n\nTarjetas gráficas con 4GiB de VRAM o menos probalemente se caeran en algún momento mientras que estén corriendo estos juegos.\n\nActivar solo si está quedan sin VRAM en los juegos antes mencionados. Desactívalo si no sabes qué hacer.", "SettingsTabGraphicsPreferredGpu": "GPU preferida", "SettingsTabGraphicsPreferredGpuTooltip": "Selecciona la tarjeta gráfica que se utilizará con los back-end de gráficos Vulkan.\n\nNo afecta la GPU que utilizará OpenGL.\n\nFije a la GPU marcada como \"dGUP\" ante dudas. Si no hay una, no haga modificaciones.", "SettingsAppRequiredRestartMessage": "Reinicio de Ryujinx requerido.", @@ -751,7 +829,7 @@ "UserProfilesManageSaves": "Administrar mis partidas guardadas", "DeleteUserSave": "¿Quieres borrar los datos de usuario de este juego?", "IrreversibleActionNote": "Esta acción no es reversible.", - "SaveManagerHeading": "Manage Saves for {0}", + "SaveManagerHeading": "Administrar partidas guardadas para {0}", "SaveManagerTitle": "Administrador de datos de guardado.", "Name": "Nombre", "Size": "Tamaño", @@ -760,13 +838,14 @@ "Recover": "Recuperar", "UserProfilesRecoverHeading": "Datos de guardado fueron encontrados para las siguientes cuentas", "UserProfilesRecoverEmptyList": "No hay perfiles a recuperar", - "GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.", + "GraphicsAATooltip": "Aplica antia-aliasing al rendereo del juego.\n\nFXAA desenfocará la mayor parte del la iamgen, mientras que SMAA intentará encontrar bordes irregulares y suavizarlos.\n\nNo se recomienda usar en conjunto con filtro de escala FSR.\n\nEsta opción puede ser modificada mientras que esté corriendo el juego haciendo click en \"Aplicar\" más abajo; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nDejar en NADA si no está seguro.", "GraphicsAALabel": "Suavizado de bordes:", "GraphicsScalingFilterLabel": "Filtro de escalado:", "GraphicsScalingFilterTooltip": "Elija el filtro de escala que se aplicará al utilizar la escala de resolución.\n\nBilinear funciona bien para juegos 3D y es una opción predeterminada segura.\n\nSe recomienda el bilinear para juegos de pixel art.\n\nFSR 1.0 es simplemente un filtro de afilado, no se recomienda su uso con FXAA o SMAA.\n\nEsta opción se puede cambiar mientras se ejecuta un juego haciendo clic en \"Aplicar\" a continuación; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nDéjelo en BILINEAR si no está seguro.", - "GraphicsScalingFilterBilinear": "Bilinear\n", + "GraphicsScalingFilterBilinear": "Bilinear", "GraphicsScalingFilterNearest": "Cercano", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "Nivel", "GraphicsScalingFilterLevelTooltip": "Ajuste el nivel de nitidez FSR 1.0. Mayor es más nítido.", "SmaaLow": "SMAA Bajo", @@ -784,6 +863,18 @@ "SettingsTabNetworkMultiplayer": "Multijugador", "MultiplayerMode": "Modo:", "MultiplayerModeTooltip": "Cambiar modo LDN multijugador.\n\nLdnMitm modificará la funcionalidad local de juego inalámbrico para funcionar como si fuera LAN, permitiendo locales conexiones de la misma red con otras instancias de Ryujinx y consolas hackeadas de Nintendo Switch que tienen instalado el módulo ldn_mitm.\n\nMultijugador requiere que todos los jugadores estén en la misma versión del juego (por ejemplo, Super Smash Bros. Ultimate v13.0.1 no se puede conectar a v13.0.0).\n\nDejar DESACTIVADO si no está seguro.", - "MultiplayerModeDisabled": "Deshabilitar", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeDisabled": "Deshabilitado", + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Desactivar El Hosteo De Red P2P (puede aumentar latencia)", + "MultiplayerDisableP2PTooltip": "Desactivar el hosteo de red P2P, pares se conectarán a través del servidor maestro en lugar de conectarse directamente contigo.", + "LdnPassphrase": "Frase de contraseña de la Red:", + "LdnPassphraseTooltip": "Solo podrás ver los juegos hosteados con la misma frase de contraseña que tú.", + "LdnPassphraseInputTooltip": "Ingresar una frase de contraseña en formato Ryujinx-<8 caracteres hexadecimales>. Solamente podrás ver juegos hosteados con la misma frase de contraseña que tú.", + "LdnPassphraseInputPublic": "(público)", + "GenLdnPass": "Generar aleatorio", + "GenLdnPassTooltip": "Genera una nueva frase de contraseña, que puede ser compartida con otros jugadores.", + "ClearLdnPass": "Borrar", + "ClearLdnPassTooltip": "Borra la frase de contraseña actual, regresando a la red pública.", + "InvalidLdnPassphrase": "Frase de Contraseña Inválida! Debe ser en formato \"Ryujinx-<8 caracteres hexadecimales>\"" } diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index df8adac00..471dfbe5e 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -1,6 +1,7 @@ { "Language": "Français", "MenuBarFileOpenApplet": "Ouvrir un programme", + "MenuBarFileOpenAppletOpenMiiApplet": "Éditeur de Mii", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Ouvrir l'éditeur Mii en mode Standalone", "SettingsTabInputDirectMouseAccess": "Accès direct à la souris", "SettingsTabSystemMemoryManagerMode": "Mode de gestion de la mémoire :", @@ -30,9 +31,13 @@ "MenuBarToolsInstallFirmware": "Installer un firmware", "MenuBarFileToolsInstallFirmwareFromFile": "Installer un firmware depuis un fichier XCI ou ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "Installer un firmware depuis un dossier", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Gérer les types de fichiers", "MenuBarToolsInstallFileTypes": "Installer les types de fichiers", "MenuBarToolsUninstallFileTypes": "Désinstaller les types de fichiers", + "MenuBarToolsXCITrimmer": "Réduire les fichiers XCI", "MenuBarView": "_Fenêtre", "MenuBarViewWindow": "Taille de la fenêtre", "MenuBarViewWindow720": "720p", @@ -84,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Ouvre le dossier contenant les mods du jeu", "GameListContextMenuOpenSdModsDirectory": "Ouvrir le dossier des mods Atmosphère", "GameListContextMenuOpenSdModsDirectoryToolTip": "Ouvre le dossier alternatif de la carte SD Atmosphère qui contient les mods de l'application. Utile pour les mods conçus pour console.", + "GameListContextMenuTrimXCI": "Vérifier et réduire les fichiers XCI", + "GameListContextMenuTrimXCIToolTip": "Vérifier et réduire les fichiers XCI pour économiser de l'espace", "StatusBarGamesLoaded": "{0}/{1} Jeux chargés", "StatusBarSystemVersion": "Version du Firmware: {0}", + "StatusBarXCIFileTrimming": "Réduction du fichier XCI '{0}'", "LinuxVmMaxMapCountDialogTitle": "Limite basse pour les mappings mémoire détectée", "LinuxVmMaxMapCountDialogTextPrimary": "Voulez-vous augmenter la valeur de vm.max_map_count à {0}", "LinuxVmMaxMapCountDialogTextSecondary": "Certains jeux peuvent essayer de créer plus de mappings mémoire que ce qui est actuellement autorisé. Ryujinx plantera dès que cette limite sera dépassée.", @@ -100,7 +108,7 @@ "SettingsTabGeneralCheckUpdatesOnLaunch": "Vérifier les mises à jour au démarrage", "SettingsTabGeneralShowConfirmExitDialog": "Afficher le message de \"Confirmation de sortie\"", "SettingsTabGeneralRememberWindowState": "Mémoriser la taille/position de la fenêtre", - "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralShowTitleBar": "Afficher Barre de Titre (Nécessite redémarrage)", "SettingsTabGeneralHideCursor": "Masquer le Curseur :", "SettingsTabGeneralHideCursorNever": "Jamais", "SettingsTabGeneralHideCursorOnIdle": "Masquer le curseur si inactif", @@ -151,7 +159,7 @@ "SettingsTabSystemAudioBackendSDL2": "SDL2", "SettingsTabSystemHacks": "Hacks", "SettingsTabSystemHacksNote": "Cela peut causer des instabilités", - "SettingsTabSystemDramSize": "Taille de la DRAM:", + "SettingsTabSystemDramSize": "Taille de la DRAM :", "SettingsTabSystemDramSize4GiB": "4GiO", "SettingsTabSystemDramSize6GiB": "6GiO", "SettingsTabSystemDramSize8GiB": "8GiO", @@ -181,7 +189,7 @@ "SettingsTabGraphicsAspectRatio32x9": "32:9", "SettingsTabGraphicsAspectRatioStretch": "Étirer pour remplir la fenêtre", "SettingsTabGraphicsDeveloperOptions": "Options développeur", - "SettingsTabGraphicsShaderDumpPath": "Chemin du dossier de copie des shaders:", + "SettingsTabGraphicsShaderDumpPath": "Chemin du dossier de copie des shaders :", "SettingsTabLogging": "Journaux", "SettingsTabLoggingLogging": "Journaux", "SettingsTabLoggingEnableLoggingToFile": "Activer la sauvegarde des journaux vers un fichier", @@ -387,7 +395,7 @@ "UserProfilesSelectedUserProfile": "Profil utilisateur sélectionné :", "UserProfilesSaveProfileName": "Enregistrer le nom du profil", "UserProfilesChangeProfileImage": "Changer l'image du profil", - "UserProfilesAvailableUserProfiles": "Profils utilisateurs disponibles:", + "UserProfilesAvailableUserProfiles": "Profils utilisateurs disponibles :", "UserProfilesAddNewProfile": "Créer un profil", "UserProfilesDelete": "Supprimer", "UserProfilesClose": "Fermer", @@ -400,6 +408,8 @@ "InputDialogTitle": "Fenêtre d'entrée de texte", "InputDialogOk": "OK", "InputDialogCancel": "Annuler", + "InputDialogCancelling": "Annulation en cours", + "InputDialogClose": "Fermer", "InputDialogAddNewProfileTitle": "Choisir un nom de profil", "InputDialogAddNewProfileHeader": "Merci d'entrer un nom de profil", "InputDialogAddNewProfileSubtext": "(Longueur max.: {0})", @@ -408,6 +418,7 @@ "AvatarClose": "Fermer", "ControllerSettingsLoadProfileToolTip": "Charger un profil", "ControllerSettingsAddProfileToolTip": "Ajouter un profil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsRemoveProfileToolTip": "Supprimer un profil", "ControllerSettingsSaveProfileToolTip": "Enregistrer un profil", "MenuBarFileToolsTakeScreenshot": "Prendre une capture d'écran", @@ -437,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "Une erreur s'est produite lors de la recherche de la sauvegarde spécifiée : {0}", "FolderDialogExtractTitle": "Choisissez le dossier dans lequel extraire", "DialogNcaExtractionMessage": "Extraction de la section {0} depuis {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Extracteur de la section NCA", + "DialogNcaExtractionTitle": "Extracteur de la section NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Échec de l'extraction. Le NCA principal n'était pas présent dans le fichier sélectionné.", "DialogNcaExtractionCheckLogErrorMessage": "Échec de l'extraction. Lisez le fichier journal pour plus d'informations.", "DialogNcaExtractionSuccessMessage": "Extraction terminée avec succès.", @@ -450,12 +461,13 @@ "DialogUpdaterExtractionMessage": "Extraction de la mise à jour…", "DialogUpdaterRenamingMessage": "Renommage de la mise à jour...", "DialogUpdaterAddingFilesMessage": "Ajout d'une nouvelle mise à jour...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Mise à jour terminée !", "DialogUpdaterRestartMessage": "Voulez-vous redémarrer Ryujinx maintenant ?", "DialogUpdaterNoInternetMessage": "Vous n'êtes pas connecté à Internet !", "DialogUpdaterNoInternetSubMessage": "Veuillez vérifier que vous disposez d'une connexion Internet fonctionnelle !", "DialogUpdaterDirtyBuildMessage": "Vous ne pouvez pas mettre à jour une version Dirty de Ryujinx !", - "DialogUpdaterDirtyBuildSubMessage": "Veuillez télécharger Ryujinx sur https://github.com/GreemDev/Ryujinx/releases/ si vous recherchez une version prise en charge.", + "DialogUpdaterDirtyBuildSubMessage": "Veuillez télécharger Ryujinx sur https://ryujinx.app/download si vous recherchez une version prise en charge.", "DialogRestartRequiredMessage": "Redémarrage requis", "DialogThemeRestartMessage": "Le thème a été enregistré. Un redémarrage est requis pour appliquer le thème.", "DialogThemeRestartSubMessage": "Voulez-vous redémarrer", @@ -468,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "Types de fichiers désinstallés avec succès!", "DialogUninstallFileTypesErrorMessage": "Échec de la désinstallation des types de fichiers.", "DialogOpenSettingsWindowLabel": "Ouvrir la fenêtre de configuration", + "DialogOpenXCITrimmerWindowLabel": "Fenêtre de réduction de fichiers XCI", "DialogControllerAppletTitle": "Programme Manette", "DialogMessageDialogErrorExceptionMessage": "Erreur lors de l'affichage de la boîte de dialogue : {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Erreur lors de l'affichage du clavier logiciel: {0}", @@ -496,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nVoulez-vous continuer ?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installation du firmware...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Version du système {0} installée avec succès.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "Il n'y aurait aucun autre profil à ouvrir si le profil sélectionné est supprimé", "DialogUserProfileDeletionConfirmMessage": "Voulez-vous supprimer le profil sélectionné ?", "DialogUserProfileUnsavedChangesTitle": "Avertissement - Modifications non enregistrées", @@ -669,13 +689,22 @@ "NoUpdate": "Aucune mise à jour", "TitleUpdateVersionLabel": "Version {0}", "TitleBundledUpdateVersionLabel": "Inclus avec le jeu: Version {0}", - "TitleBundledDlcLabel": "Inclus avec le jeu:", + "TitleBundledDlcLabel": "Inclus avec le jeu :", + "TitleXCIStatusPartialLabel": "Partiel", + "TitleXCIStatusTrimmableLabel": "Non réduit", + "TitleXCIStatusUntrimmableLabel": "Réduit", + "TitleXCIStatusFailedLabel": "(Échoué)", + "TitleXCICanSaveLabel": "Sauvegarde de {0:n0} Mo", + "TitleXCISavingLabel": "Sauvegardé {0:n0} Mo", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Confirmation", "FileDialogAllTypes": "Tous les types", "Never": "Jamais", "SwkbdMinCharacters": "Doit comporter au moins {0} caractères", "SwkbdMinRangeCharacters": "Doit comporter entre {0} et {1} caractères", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Clavier logiciel", "SoftwareKeyboardModeNumeric": "Doit être 0-9 ou '.' uniquement", "SoftwareKeyboardModeAlphabet": "Doit être uniquement des caractères non CJK", @@ -722,11 +751,39 @@ "SelectDlcDialogTitle": "Sélectionner les fichiers DLC", "SelectUpdateDialogTitle": "Sélectionner les fichiers de mise à jour", "SelectModDialogTitle": "Sélectionner le répertoire du mod", + "TrimXCIFileDialogTitle": "Vérifier et Réduire le fichier XCI", + "TrimXCIFileDialogPrimaryText": "Cette fonction va vérifier l'espace vide, puis réduire le fichier XCI pour économiser de l'espace de disque dur.", + "TrimXCIFileDialogSecondaryText": "Taille actuelle du fichier: {0:n} MB\nTaille des données de jeux: {1:n} MB\nÉconomie d'espaces sur le disque: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "Fichier XCI n'a pas besoin d'être réduit. Regarder les journaux pour plus de détails", + "TrimXCIFileNoUntrimPossible": "Fichier XCI ne peut pas être dé-réduit. Regarder les journaux pour plus de détails", + "TrimXCIFileReadOnlyFileCannotFix": "Fichier XCI est en Lecture Seule et n'a pas pu être rendu accessible en écriture. Regarder les journaux pour plus de détails", + "TrimXCIFileFileSizeChanged": "Fichier XCI a changé en taille depuis qu'il a été scanné. Vérifier que le fichier n'est pas en cours d'écriture et réessayer.", + "TrimXCIFileFreeSpaceCheckFailed": "Fichier XCI a des données dans la zone d'espace libre, ce n'est pas sûr de réduire", + "TrimXCIFileInvalidXCIFile": "Fichier XCI contient des données invalides. Regarder les journaux pour plus de détails", + "TrimXCIFileFileIOWriteError": "Fichier XCI n'a pas pu été ouvert pour écriture. Regarder les journaux pour plus de détails", + "TrimXCIFileFailedPrimaryText": "Réduction du fichier XCI a échoué", + "TrimXCIFileCancelled": "L'opération a été annulée", + "TrimXCIFileFileUndertermined": "Aucune opération a été faite", "UserProfileWindowTitle": "Gestionnaire de profils utilisateur", "CheatWindowTitle": "Gestionnaire de cheats", "DlcWindowTitle": "Gérer le contenu téléchargeable pour {0} ({1})", "ModWindowTitle": "Gérer les mods pour {0} ({1})", "UpdateWindowTitle": "Gestionnaire de mises à jour", + "XCITrimmerWindowTitle": "Rogneur de fichier XCI", + "XCITrimmerTitleStatusCount": "{0} sur {1} Fichier(s) Sélectionnés", + "XCITrimmerTitleStatusCountWithFilter": "{0} sur {1} Fichier(s) Sélectionnés ({2} affiché(s)", + "XCITrimmerTitleStatusTrimming": "Réduction de {0} Fichier(s)...", + "XCITrimmerTitleStatusUntrimming": "Dé-Réduction de {0} Fichier(s)...", + "XCITrimmerTitleStatusFailed": "Échoué", + "XCITrimmerPotentialSavings": "Économies potentielles d'espace de disque dur", + "XCITrimmerActualSavings": "Économies actualles d'espace de disque dur", + "XCITrimmerSavingsMb": "{0:n0} Mo", + "XCITrimmerSelectDisplayed": "Sélectionner Affiché", + "XCITrimmerDeselectDisplayed": "Désélectionner Affiché", + "XCITrimmerSortName": "Titre", + "XCITrimmerSortSaved": "Économies de disque dur", + "XCITrimmerTrim": "Réduire", + "XCITrimmerUntrim": "Dé-Réduire", "UpdateWindowUpdateAddedMessage": "{0} nouvelle(s) mise(s) à jour ajoutée(s)", "UpdateWindowBundledContentNotice": "Les mises à jour incluses avec le jeu ne peuvent pas être supprimées mais peuvent être désactivées.", "CheatWindowHeading": "Cheats disponibles pour {0} [{1}]", @@ -740,6 +797,7 @@ "AutoloadUpdateRemovedMessage": "{0} mises à jour manquantes supprimées", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Éditer la sélection", + "Continue": "Continuer", "Cancel": "Annuler", "Save": "Enregistrer", "Discard": "Abandonner", @@ -807,5 +865,17 @@ "MultiplayerMode": "Mode :", "MultiplayerModeTooltip": "Changer le mode multijoueur LDN.\n\nLdnMitm modifiera la fonctionnalité de jeu sans fil local/jeu local dans les jeux pour fonctionner comme s'il s'agissait d'un LAN, permettant des connexions locales sur le même réseau avec d'autres instances de Ryujinx et des consoles Nintendo Switch piratées ayant le module ldn_mitm installé.\n\nLe multijoueur nécessite que tous les joueurs soient sur la même version du jeu (par exemple, Super Smash Bros. Ultimate v13.0.1 ne peut pas se connecter à v13.0.0).\n\nLaissez DÉSACTIVÉ si vous n'êtes pas sûr.", "MultiplayerModeDisabled": "Désactivé", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Désactiver PàP Hébergement de Réseau (pourrait augmenter la latence)", + "MultiplayerDisableP2PTooltip": "Désactiver PàP hébergement de réseau, les postes vont proxy avec le serveur principal au lieu de se connecter directement à vous.", + "LdnPassphrase": "Mot de passe Réseau :", + "LdnPassphraseTooltip": "Vous pourez seulement voir les jeux hébergé avec le même mot de passe que vous.", + "LdnPassphraseInputTooltip": "Entrer un mot de passe dans le format Ryujinx-<8 hex chars>. Vous pourez seulement voir les jeux hébergé avec le même mot de passe que vous.", + "LdnPassphraseInputPublic": "(publique)", + "GenLdnPass": "Générer Aléatoire", + "GenLdnPassTooltip": "Génére un nouveau mot de passe, qui peut être partagé avec les autres.", + "ClearLdnPass": "Supprimer", + "ClearLdnPassTooltip": "Supprime le mot de passe actuel, ce qui vous remet sur le réseau public.", + "InvalidLdnPassphrase": "Mot de passe invalide! Il doit être dans le format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json index eb7ccf322..dbacf5ea1 100644 --- a/src/Ryujinx/Assets/Locales/he_IL.json +++ b/src/Ryujinx/Assets/Locales/he_IL.json @@ -1,6 +1,7 @@ { "Language": "עִברִית", "MenuBarFileOpenApplet": "פתח יישומון", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "פתח את יישומון עורך ה- Mii במצב עצמאי", "SettingsTabInputDirectMouseAccess": "גישה ישירה לעכבר", "SettingsTabSystemMemoryManagerMode": "מצב מנהל זיכרון:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "השתמש ב Hypervisor", "MenuBarFile": "_קובץ", "MenuBarFileOpenFromFile": "_טען יישום מקובץ", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "טען משחק _שאינו ארוז", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "פתח את תיקיית ריוג'ינקס", "MenuBarFileOpenLogsFolder": "פתח את תיקיית קבצי הלוג", "MenuBarFileExit": "_יציאה", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "התקן קושחה", "MenuBarFileToolsInstallFirmwareFromFile": "התקן קושחה מקובץ- ZIP/XCI", "MenuBarFileToolsInstallFirmwareFromDirectory": "התקן קושחה מתוך תקייה", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "ניהול סוגי קבצים", "MenuBarToolsInstallFileTypes": "סוגי קבצי התקנה", "MenuBarToolsUninstallFileTypes": "סוגי קבצי הסרה", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_View", "MenuBarViewWindow": "Window Size", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "פותח את התיקייה שמכילה מודים של האפליקציה", "GameListContextMenuOpenSdModsDirectory": "פתח תיקיית מודים של Atmosphere", "GameListContextMenuOpenSdModsDirectoryToolTip": "פותח את תיקיית כרטיס ה-SD החלופית של Atmosphere המכילה את המודים של האפליקציה. שימושי עבור מודים שארוזים עבור חומרה אמיתית.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{1}/{0} משחקים נטענו", "StatusBarSystemVersion": "גרסת מערכת: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "זוהתה מגבלה נמוכה עבור מיפויי זיכרון", "LinuxVmMaxMapCountDialogTextPrimary": "האם תרצה להגביר את הערך של vm.max_map_count ל{0}", "LinuxVmMaxMapCountDialogTextSecondary": "משחקים מסוימים עלולים לייצר עוד מיפויי זיכרון ממה שמתאפשר. Ryujinx יקרוס ברגע שהמגבלה תחרוג.", @@ -103,6 +114,8 @@ "SettingsTabGeneralHideCursorOnIdle": "במצב סרק", "SettingsTabGeneralHideCursorAlways": "תמיד", "SettingsTabGeneralGameDirectories": "תקיות משחקים", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "הוסף", "SettingsTabGeneralRemove": "הסר", "SettingsTabSystem": "מערכת", @@ -395,6 +408,8 @@ "InputDialogTitle": "דיאלוג קלט", "InputDialogOk": "בסדר", "InputDialogCancel": "ביטול", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "בחרו את שם הפרופיל", "InputDialogAddNewProfileHeader": "אנא הזינו שם לפרופיל", "InputDialogAddNewProfileSubtext": "(אורך מרבי: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "הגדר צבע רקע", "AvatarClose": "סגור", "ControllerSettingsLoadProfileToolTip": "טען פרופיל", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "הוסף פרופיל", "ControllerSettingsRemoveProfileToolTip": "הסר פרופיל", "ControllerSettingsSaveProfileToolTip": "שמור פרופיל", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "למתג העדפה", "GameListContextMenuToggleFavoriteToolTip": "למתג סטטוס העדפה של משחק", "SettingsTabGeneralTheme": "ערכת נושא:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "כהה", "SettingsTabGeneralThemeLight": "בהיר", "ControllerSettingsConfigureGeneral": "הגדר", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "אירעה שגיאה במציאת שמור המשחק שצויין: {0}", "FolderDialogExtractTitle": "בחרו את התיקייה לחילוץ", "DialogNcaExtractionMessage": "מלחץ {0} ממקטע {1}...", - "DialogNcaExtractionTitle": "ריוג'ינקס - מחלץ מקטע NCA", + "DialogNcaExtractionTitle": "מחלץ מקטע NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "כשל בחילוץ. ה-NCA הראשי לא היה קיים בקובץ שנבחר.", "DialogNcaExtractionCheckLogErrorMessage": "כשל בחילוץ. קרא את קובץ הרישום למידע נוסף.", "DialogNcaExtractionSuccessMessage": "החילוץ הושלם בהצלחה.", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "מחלץ עדכון...", "DialogUpdaterRenamingMessage": "משנה את שם העדכון...", "DialogUpdaterAddingFilesMessage": "מוסיף עדכון חדש...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "העדכון הושלם!", "DialogUpdaterRestartMessage": "האם אתם רוצים להפעיל מחדש את ריוג'ינקס עכשיו?", "DialogUpdaterNoInternetMessage": "אתם לא מחוברים לאינטרנט!", "DialogUpdaterNoInternetSubMessage": "אנא ודא שיש לך חיבור אינטרנט תקין!", "DialogUpdaterDirtyBuildMessage": "אתם לא יכולים לעדכן מבנה מלוכלך של ריוג'ינקס!", - "DialogUpdaterDirtyBuildSubMessage": "אם אתם מחפשים גרסא נתמכת, אנא הורידו את ריוג'ינקס בכתובת https://https://github.com/GreemDev/Ryujinx/releases", + "DialogUpdaterDirtyBuildSubMessage": "אם אתם מחפשים גרסא נתמכת, אנא הורידו את ריוג'ינקס בכתובת https://ryujinx.app/download", "DialogRestartRequiredMessage": "אתחול נדרש", "DialogThemeRestartMessage": "ערכת הנושא נשמרה. יש צורך בהפעלה מחדש כדי להחיל את ערכת הנושא.", "DialogThemeRestartSubMessage": "האם ברצונך להפעיל מחדש?", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "סוגי קבצים הוסרו בהצלחה!", "DialogUninstallFileTypesErrorMessage": "נכשל בהסרת סוגי קבצים.", "DialogOpenSettingsWindowLabel": "פתח את חלון ההגדרות", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "יישומון בקר", "DialogMessageDialogErrorExceptionMessage": "שגיאה בהצגת דיאלוג ההודעה: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "שגיאה בהצגת תוכנת המקלדת: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nהאם ברצונך להמשיך?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "מתקין קושחה...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "גרסת המערכת {0} הותקנה בהצלחה.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "לא יהיו פרופילים אחרים שייפתחו אם הפרופיל שנבחר יימחק", "DialogUserProfileDeletionConfirmMessage": "האם ברצונך למחוק את הפרופיל שנבחר", "DialogUserProfileUnsavedChangesTitle": "אזהרה - שינויים לא שמורים", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "הזן תקיית משחקים כדי להוסיף לרשימה", "AddGameDirTooltip": "הוסף תקיית משחקים לרשימה", "RemoveGameDirTooltip": "הסר את תקיית המשחקים שנבחרה", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "השתמש בעיצוב מותאם אישית של אבלוניה עבור ה-ממשק הגראפי כדי לשנות את המראה של תפריטי האמולטור", "CustomThemePathTooltip": "נתיב לערכת נושא לממשק גראפי מותאם אישית", "CustomThemeBrowseTooltip": "חפש עיצוב ממשק גראפי מותאם אישית", @@ -606,6 +635,8 @@ "DebugLogTooltip": "מדפיס הודעות יומן ניפוי באגים בשורת הפקודות.", "LoadApplicationFileTooltip": "פתח סייר קבצים כדי לבחור קובץ תואם סוויץ' לטעינה", "LoadApplicationFolderTooltip": "פתח סייר קבצים כדי לבחור יישום תואם סוויץ', לא ארוז לטעינה.", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "פתח את תיקיית מערכת הקבצים ריוג'ינקס", "OpenRyujinxLogsTooltip": "פותח את התיקיה שאליה נכתבים רישומים", "ExitTooltip": "צא מריוג'ינקס", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "פתח מדריך התקנה", "NoUpdate": "אין עדכון", "TitleUpdateVersionLabel": "גרסה {0}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "ריוג'ינקס - מידע", "RyujinxConfirm": "ריוג'ינקס - אישור", "FileDialogAllTypes": "כל הסוגים", "Never": "אף פעם", "SwkbdMinCharacters": "לפחות {0} תווים", "SwkbdMinRangeCharacters": "באורך {0}-{1} תווים", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "מקלדת וירטואלית", "SoftwareKeyboardModeNumeric": "חייב להיות בין 0-9 או '.' בלבד", "SoftwareKeyboardModeAlphabet": "מחויב להיות ללא אותיות CJK", @@ -709,16 +751,53 @@ "SelectDlcDialogTitle": "בחרו קבצי הרחבות משחק", "SelectUpdateDialogTitle": "בחרו קבצי עדכון", "SelectModDialogTitle": "בחר תיקיית מודים", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "ניהול פרופילי משתמש", "CheatWindowTitle": "נהל צ'יטים למשחק", "DlcWindowTitle": "נהל הרחבות משחק עבור {0} ({1})", "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "נהל עדכוני משחקים", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "צ'יטים זמינים עבור {0} [{1}]", "BuildId": "מזהה בניה:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "DlcWindowHeading": "{0} הרחבות משחק", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} מוד(ים)", "UserProfilesEditProfile": "ערוך נבחר/ים", + "Continue": "Continue", "Cancel": "בטל", "Save": "שמור", "Discard": "השלך", @@ -767,6 +846,7 @@ "GraphicsScalingFilterBilinear": "Bilinear", "GraphicsScalingFilterNearest": "Nearest", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "רמה", "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.", "SmaaLow": "SMAA נמוך", @@ -785,5 +865,17 @@ "MultiplayerMode": "מצב:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", "MultiplayerModeDisabled": "Disabled", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json index 87c8e6bab..61ea2a355 100644 --- a/src/Ryujinx/Assets/Locales/it_IT.json +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -1,6 +1,7 @@ { "Language": "Italiano", "MenuBarFileOpenApplet": "Apri applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Apri l'applet Mii Editor in modalità Standalone", "SettingsTabInputDirectMouseAccess": "Accesso diretto al mouse", "SettingsTabSystemMemoryManagerMode": "Modalità di gestione della memoria:", @@ -27,14 +28,18 @@ "MenuBarToolsInstallFirmware": "Installa firmware", "MenuBarFileToolsInstallFirmwareFromFile": "Installa un firmware da file XCI o ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "Installa un firmare da una cartella", + "MenuBarToolsInstallKeys": "Installa Chiavi", + "MenuBarFileToolsInstallKeysFromFile": "Installa Chiavi da file KEYS o ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Installa Chiavi da una Cartella", "MenuBarToolsManageFileTypes": "Gestisci i tipi di file", "MenuBarToolsInstallFileTypes": "Installa i tipi di file", "MenuBarToolsUninstallFileTypes": "Disinstalla i tipi di file", "MenuBarFileLoadDlcFromFolder": "Carica DLC Da una Cartella", "MenuBarFileLoadTitleUpdatesFromFolder": "Carica Aggiornamenti Da una Cartella", "MenuBarFileOpenFromFileError": "Nessuna applicazione trovata nel file selezionato", - "MenuBarView": "_View", - "MenuBarViewWindow": "Window Size", + "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarView": "_Vista", + "MenuBarViewWindow": "Dimensione Finestra", "MenuBarViewWindow720": "720p", "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "_Aiuto", @@ -84,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Apre la cartella che contiene le mod dell'applicazione", "GameListContextMenuOpenSdModsDirectory": "Apri la cartella delle mod Atmosphere", "GameListContextMenuOpenSdModsDirectoryToolTip": "Apre la cartella alternativa di Atmosphere sulla scheda SD che contiene le mod dell'applicazione. Utile per le mod create per funzionare sull'hardware reale.", - "StatusBarGamesLoaded": "{0}/{1} giochi caricati", + "GameListContextMenuTrimXCI": "Controlla e Trimma i file XCI", + "GameListContextMenuTrimXCIToolTip": "Controlla e Trimma i file XCI da Salvare Sullo Spazio del Disco", + "StatusBarGamesLoaded": "{0}/{1} Giochi Caricati", "StatusBarSystemVersion": "Versione di sistema: {0}", + "StatusBarXCIFileTrimming": "Trimmando i file XCI '{0}'", "LinuxVmMaxMapCountDialogTitle": "Rilevato limite basso per le mappature di memoria", "LinuxVmMaxMapCountDialogTextPrimary": "Vuoi aumentare il valore di vm.max_map_count a {0}?", "LinuxVmMaxMapCountDialogTextSecondary": "Alcuni giochi potrebbero provare a creare più mappature di memoria di quanto sia attualmente consentito. Ryujinx si bloccherà non appena questo limite viene superato.", @@ -99,8 +107,8 @@ "SettingsTabGeneralEnableDiscordRichPresence": "Attiva Discord Rich Presence", "SettingsTabGeneralCheckUpdatesOnLaunch": "Controlla aggiornamenti all'avvio", "SettingsTabGeneralShowConfirmExitDialog": "Mostra dialogo \"Conferma Uscita\"", - "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position", - "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralRememberWindowState": "Ricorda Dimensione/Posizione Finestra", + "SettingsTabGeneralShowTitleBar": "Mostra barra del titolo (Richiede il riavvio)", "SettingsTabGeneralHideCursor": "Nascondi il cursore:", "SettingsTabGeneralHideCursorNever": "Mai", "SettingsTabGeneralHideCursorOnIdle": "Quando è inattivo", @@ -400,6 +408,8 @@ "InputDialogTitle": "Finestra di input", "InputDialogOk": "OK", "InputDialogCancel": "Annulla", + "InputDialogCancelling": "Cancellando", + "InputDialogClose": "Chiudi", "InputDialogAddNewProfileTitle": "Scegli il nome del profilo", "InputDialogAddNewProfileHeader": "Digita un nome profilo", "InputDialogAddNewProfileSubtext": "(Lunghezza massima: {0})", @@ -407,6 +417,7 @@ "AvatarSetBackgroundColor": "Imposta colore di sfondo", "AvatarClose": "Chiudi", "ControllerSettingsLoadProfileToolTip": "Carica profilo", + "ControllerSettingsViewProfileToolTip": "Visualizza profilo", "ControllerSettingsAddProfileToolTip": "Aggiungi profilo", "ControllerSettingsRemoveProfileToolTip": "Rimuovi profilo", "ControllerSettingsSaveProfileToolTip": "Salva profilo", @@ -416,7 +427,7 @@ "GameListContextMenuToggleFavorite": "Preferito", "GameListContextMenuToggleFavoriteToolTip": "Segna il gioco come preferito", "SettingsTabGeneralTheme": "Tema:", - "SettingsTabGeneralThemeAuto": "Auto", + "SettingsTabGeneralThemeAuto": "Automatico", "SettingsTabGeneralThemeDark": "Scuro", "SettingsTabGeneralThemeLight": "Chiaro", "ControllerSettingsConfigureGeneral": "Configura", @@ -437,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "C'è stato un errore durante la ricerca dei dati di salvataggio: {0}", "FolderDialogExtractTitle": "Scegli una cartella in cui estrarre", "DialogNcaExtractionMessage": "Estrazione della sezione {0} da {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Estrazione sezione NCA", + "DialogNcaExtractionTitle": "Estrazione sezione NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "L'estrazione è fallita. L'NCA principale non era presente nel file selezionato.", "DialogNcaExtractionCheckLogErrorMessage": "L'estrazione è fallita. Consulta il file di log per maggiori informazioni.", "DialogNcaExtractionSuccessMessage": "Estrazione completata con successo.", @@ -450,12 +461,13 @@ "DialogUpdaterExtractionMessage": "Estrazione dell'aggiornamento...", "DialogUpdaterRenamingMessage": "Rinominazione dell'aggiornamento...", "DialogUpdaterAddingFilesMessage": "Aggiunta del nuovo aggiornamento...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Aggiornamento completato!", "DialogUpdaterRestartMessage": "Vuoi riavviare Ryujinx adesso?", "DialogUpdaterNoInternetMessage": "Non sei connesso ad Internet!", "DialogUpdaterNoInternetSubMessage": "Verifica di avere una connessione ad Internet funzionante!", "DialogUpdaterDirtyBuildMessage": "Non puoi aggiornare una Dirty build di Ryujinx!", - "DialogUpdaterDirtyBuildSubMessage": "Scarica Ryujinx da https://https://github.com/GreemDev/Ryujinx/releases/ se stai cercando una versione supportata.", + "DialogUpdaterDirtyBuildSubMessage": "Scarica Ryujinx da https://ryujinx.app/download se stai cercando una versione supportata.", "DialogRestartRequiredMessage": "Riavvio richiesto", "DialogThemeRestartMessage": "Il tema è stato salvato. È richiesto un riavvio per applicare il tema.", "DialogThemeRestartSubMessage": "Vuoi riavviare?", @@ -468,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "Tipi di file disinstallati con successo!", "DialogUninstallFileTypesErrorMessage": "Disinstallazione dei tipi di file non riuscita.", "DialogOpenSettingsWindowLabel": "Apri finestra delle impostazioni", + "DialogOpenXCITrimmerWindowLabel": "Finestra XCI Trimmer", "DialogControllerAppletTitle": "Applet del controller", "DialogMessageDialogErrorExceptionMessage": "Errore nella visualizzazione del Message Dialog: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Errore nella visualizzazione della tastiera software: {0}", @@ -496,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nVuoi continuare?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installazione del firmware...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "La versione del sistema {0} è stata installata.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "E' stato trovato un file di chiavi invalido ' {0}", + "DialogKeysInstallerKeysInstallTitle": "Installa Chavi", + "DialogKeysInstallerKeysInstallMessage": "Un nuovo file di Chiavi sarà intallato.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nQuesto potrebbe sovrascrivere alcune delle Chiavi già installate.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nVuoi continuare?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installando le chiavi...", + "DialogKeysInstallerKeysInstallSuccessMessage": "Nuovo file di chiavi installato con successo.", "DialogUserProfileDeletionWarningMessage": "Non ci sarebbero altri profili da aprire se il profilo selezionato viene cancellato", "DialogUserProfileDeletionConfirmMessage": "Vuoi eliminare il profilo selezionato?", "DialogUserProfileUnsavedChangesTitle": "Attenzione - Modifiche Non Salvate", @@ -521,7 +541,7 @@ "DialogModManagerDeletionAllWarningMessage": "Stai per eliminare tutte le mod per questo titolo.\n\nVuoi davvero procedere?", "SettingsTabGraphicsFeaturesOptions": "Funzionalità", "SettingsTabGraphicsBackendMultithreading": "Multithreading del backend grafico:", - "CommonAuto": "Auto", + "CommonAuto": "Automatico", "CommonOff": "Disattivato", "CommonOn": "Attivo", "InputDialogYes": "Sì", @@ -668,14 +688,23 @@ "OpenSetupGuideMessage": "Apri la guida all'installazione", "NoUpdate": "Nessun aggiornamento", "TitleUpdateVersionLabel": "Versione {0}", - "TitleBundledUpdateVersionLabel": "Incluso: Version {0}", - "TitleBundledDlcLabel": "Incluso:", - "RyujinxInfo": "Ryujinx - Info", + "TitleBundledUpdateVersionLabel": "In bundle: Versione {0}", + "TitleBundledDlcLabel": "In bundle:", + "TitleXCIStatusPartialLabel": "Parziale", + "TitleXCIStatusTrimmableLabel": "Non Trimmato", + "TitleXCIStatusUntrimmableLabel": "Trimmato", + "TitleXCIStatusFailedLabel": "(Fallito)", + "TitleXCICanSaveLabel": "Salva {0:n0} Mb", + "TitleXCISavingLabel": "Salva {0:n0} Mb", + "RyujinxInfo": "Ryujinx - Informazioni", "RyujinxConfirm": "Ryujinx - Conferma", "FileDialogAllTypes": "Tutti i tipi", "Never": "Mai", "SwkbdMinCharacters": "Non può avere meno di {0} caratteri", "SwkbdMinRangeCharacters": "Può avere da {0} a {1} caratteri", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Tastiera software", "SoftwareKeyboardModeNumeric": "Deve essere solo 0-9 o '.'", "SoftwareKeyboardModeAlphabet": "Deve essere solo caratteri non CJK", @@ -722,27 +751,56 @@ "SelectDlcDialogTitle": "Seleziona file dei DLC", "SelectUpdateDialogTitle": "Seleziona file di aggiornamento", "SelectModDialogTitle": "Seleziona cartella delle mod", + "TrimXCIFileDialogTitle": "Controlla e Trimma i file XCI ", + "TrimXCIFileDialogPrimaryText": "Questa funzionalita controllerà prima lo spazio libero e poi trimmerà il file XCI per liberare dello spazio.", + "TrimXCIFileDialogSecondaryText": "Dimensioni Attuali File: {0:n} MB\nDimensioni Dati Gioco: {1:n} MB\nRisparimio Spazio Disco: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "Il file XCI non deve essere trimmato. Controlla i log per ulteriori dettagli", + "TrimXCIFileNoUntrimPossible": "Il file XCI non può essere untrimmato. Controlla i log per ulteriori dettagli", + "TrimXCIFileReadOnlyFileCannotFix": "Il file XCI è in sola lettura e non può essere reso Scrivibile. Controlla i log per ulteriori dettagli", + "TrimXCIFileFileSizeChanged": "Il file XCI ha cambiato dimensioni da quando è stato scansionato. Controlla che il file non stia venendo scritto da qualche altro programma e poi riprova.", + "TrimXCIFileFreeSpaceCheckFailed": "Il file XCI ha dati nello spazio libero, non è sicuro effettuare il trimming", + "TrimXCIFileInvalidXCIFile": "Il file XCI contiene dati invlidi. Controlla i log per ulteriori dettagli", + "TrimXCIFileFileIOWriteError": "Il file XCI non può essere aperto per essere scritto. Controlla i log per ulteriori dettagli", + "TrimXCIFileFailedPrimaryText": "Trimming del file XCI fallito", + "TrimXCIFileCancelled": "Operazione Cancellata", + "TrimXCIFileFileUndertermined": "Nessuna operazione è stata effettuata", "UserProfileWindowTitle": "Gestione profili utente", "CheatWindowTitle": "Gestione trucchi", "DlcWindowTitle": "Gestisci DLC per {0} ({1})", "ModWindowTitle": "Gestisci mod per {0} ({1})", "UpdateWindowTitle": "Gestione aggiornamenti", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} di {1} Titolo(i) Selezionati", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Titolo(i) Selezionati ({2} visualizzato)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Titolo(i)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Titolo(i)...", + "XCITrimmerTitleStatusFailed": "Fallito", + "XCITrimmerPotentialSavings": "Potenziali Salvataggi", + "XCITrimmerActualSavings": "Effettivi Salvataggi", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Seleziona Visualizzati", + "XCITrimmerDeselectDisplayed": "Deselziona Visualizzati", + "XCITrimmerSortName": "Titolo", + "XCITrimmerSortSaved": "Salvataggio Spazio", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} aggiornamento/i aggiunto/i", + "UpdateWindowBundledContentNotice": "Gli aggiornamenti inclusi non possono essere eliminati, ma solo disattivati", "CheatWindowHeading": "Trucchi disponibili per {0} [{1}]", "BuildId": "ID Build", + "DlcWindowBundledContentNotice": "i DLC \"impacchettati\" non possono essere rimossi, ma solo disabilitati.", "DlcWindowHeading": "DLC disponibili per {0} [{1}]", - "ModWindowHeading": "{0} mod", - "UserProfilesEditProfile": "Modifica selezionati", - "Cancel": "Annulla", - "Save": "Salva", - "Discard": "Scarta", - "UpdateWindowBundledContentNotice": "Gli aggiornamenti inclusi non possono essere eliminati, ma solo disattivati", + "DlcWindowDlcAddedMessage": "{0} nuovo/i contenuto/i scaricabile/i aggiunto/i", "AutoloadDlcAddedMessage": "{0} contenuto/i scaricabile/i aggiunto/i", "AutoloadDlcRemovedMessage": "{0} contenuto/i scaricabile/i mancante/i rimosso/i", "AutoloadUpdateAddedMessage": "{0} aggiornamento/i aggiunto/i", "AutoloadUpdateRemovedMessage": "{0} aggiornamento/i mancante/i rimosso/i", - "DlcWindowBundledContentNotice": "i DLC \"impacchettati\" non possono essere rimossi, ma solo disabilitati.", - "DlcWindowDlcAddedMessage": "{0} nuovo/i contenuto/i scaricabile/i aggiunto/i", - "UpdateWindowUpdateAddedMessage": "{0} aggiornamento/i aggiunto/i", + "ModWindowHeading": "{0} mod", + "UserProfilesEditProfile": "Modifica selezionati", + "Continue": "Continua", + "Cancel": "Annulla", + "Save": "Salva", + "Discard": "Scarta", "Paused": "In pausa", "UserProfilesSetProfileImage": "Imposta immagine profilo", "UserProfileEmptyNameError": "Il nome è obbligatorio", @@ -788,12 +846,12 @@ "GraphicsScalingFilterBilinear": "Bilineare", "GraphicsScalingFilterNearest": "Nearest", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "Livello", "GraphicsScalingFilterLevelTooltip": "Imposta il livello di nitidezza di FSR 1.0. Valori più alti comportano una maggiore nitidezza.", "SmaaLow": "SMAA Basso", "SmaaMedium": "SMAA Medio", "SmaaHigh": "SMAA Alto", - "GraphicsScalingFilterArea": "Area", "SmaaUltra": "SMAA Ultra", "UserEditorTitle": "Modificare L'Utente", "UserEditorTitleCreate": "Crea Un Utente", @@ -807,5 +865,17 @@ "MultiplayerMode": "Modalità:", "MultiplayerModeTooltip": "Cambia la modalità multigiocatore LDN.\n\nLdnMitm modificherà la funzionalità locale wireless/local play nei giochi per funzionare come se fosse in modalità LAN, consentendo connessioni locali sulla stessa rete con altre istanze di Ryujinx e console Nintendo Switch modificate che hanno il modulo ldn_mitm installato.\n\nLa modalità multigiocatore richiede che tutti i giocatori usino la stessa versione del gioco (es. Super Smash Bros. Ultimate v13.0.1 non può connettersi con la v13.0.0).\n\nNel dubbio, lascia l'opzione su Disabilitato.", "MultiplayerModeDisabled": "Disabilitato", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json index d43dedc2a..9acd1c486 100644 --- a/src/Ryujinx/Assets/Locales/ja_JP.json +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -1,6 +1,7 @@ { "Language": "日本語", "MenuBarFileOpenApplet": "アプレットを開く", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "スタンドアロンモードで Mii エディタアプレットを開きます", "SettingsTabInputDirectMouseAccess": "マウス直接アクセス", "SettingsTabSystemMemoryManagerMode": "メモリ管理モード:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "ハイパーバイザーを使用", "MenuBarFile": "ファイル(_F)", "MenuBarFileOpenFromFile": "ファイルからアプリケーションをロード(_L)", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "展開されたゲームをロード", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "Ryujinx フォルダを開く", "MenuBarFileOpenLogsFolder": "ログフォルダを開く", "MenuBarFileExit": "終了(_E)", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "ファームウェアをインストール", "MenuBarFileToolsInstallFirmwareFromFile": "XCI または ZIP からファームウェアをインストール", "MenuBarFileToolsInstallFirmwareFromDirectory": "ディレクトリからファームウェアをインストール", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "ファイル形式を管理", "MenuBarToolsInstallFileTypes": "ファイル形式をインストール", "MenuBarToolsUninstallFileTypes": "ファイル形式をアンインストール", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_View", "MenuBarViewWindow": "Window Size", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "アプリケーションの Mod データを格納するディレクトリを開きます", "GameListContextMenuOpenSdModsDirectory": "Atmosphere Mods ディレクトリを開く", "GameListContextMenuOpenSdModsDirectoryToolTip": "アプリケーションの Mod データを格納する SD カードの Atmosphere ディレクトリを開きます. 実際のハードウェア用に作成された Mod データに有用です.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} ゲーム", "StatusBarSystemVersion": "システムバージョン: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "メモリマッピング上限値が小さすぎます", "LinuxVmMaxMapCountDialogTextPrimary": "vm.max_map_count の値を {0}に増やしますか?", "LinuxVmMaxMapCountDialogTextSecondary": "ゲームによっては, 現在許可されているサイズより大きなメモリマッピングを作成しようとすることがあります. この制限を超えると, Ryjinx はすぐにクラッシュします.", @@ -103,6 +114,8 @@ "SettingsTabGeneralHideCursorOnIdle": "アイドル時", "SettingsTabGeneralHideCursorAlways": "常時", "SettingsTabGeneralGameDirectories": "ゲームディレクトリ", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "追加", "SettingsTabGeneralRemove": "削除", "SettingsTabSystem": "システム", @@ -395,6 +408,8 @@ "InputDialogTitle": "入力ダイアログ", "InputDialogOk": "OK", "InputDialogCancel": "キャンセル", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "プロファイル名を選択", "InputDialogAddNewProfileHeader": "プロファイル名を入力してください", "InputDialogAddNewProfileSubtext": "(最大長: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "背景色を指定", "AvatarClose": "閉じる", "ControllerSettingsLoadProfileToolTip": "プロファイルをロード", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "プロファイルを追加", "ControllerSettingsRemoveProfileToolTip": "プロファイルを削除", "ControllerSettingsSaveProfileToolTip": "プロファイルをセーブ", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "お気に入りを切り替え", "GameListContextMenuToggleFavoriteToolTip": "ゲームをお気に入りに含めるかどうかを切り替えます", "SettingsTabGeneralTheme": "テーマ:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "ダーク", "SettingsTabGeneralThemeLight": "ライト", "ControllerSettingsConfigureGeneral": "設定", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "セーブデータ: {0} の検索中にエラーが発生しました", "FolderDialogExtractTitle": "展開フォルダを選択", "DialogNcaExtractionMessage": "{1} から {0} セクションを展開中...", - "DialogNcaExtractionTitle": "Ryujinx - NCA セクション展開", + "DialogNcaExtractionTitle": "NCA セクション展開", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "展開に失敗しました. 選択されたファイルにはメイン NCA が存在しません.", "DialogNcaExtractionCheckLogErrorMessage": "展開に失敗しました. 詳細はログを確認してください.", "DialogNcaExtractionSuccessMessage": "展開が正常終了しました", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "アップデートを展開中...", "DialogUpdaterRenamingMessage": "アップデートをリネーム中...", "DialogUpdaterAddingFilesMessage": "新規アップデートを追加中...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "アップデート完了!", "DialogUpdaterRestartMessage": "すぐに Ryujinx を再起動しますか?", "DialogUpdaterNoInternetMessage": "インターネットに接続されていません!", "DialogUpdaterNoInternetSubMessage": "インターネット接続が正常動作しているか確認してください!", "DialogUpdaterDirtyBuildMessage": "Dirty ビルドの Ryujinx はアップデートできません!", - "DialogUpdaterDirtyBuildSubMessage": "サポートされているバージョンをお探しなら, https://https://github.com/GreemDev/Ryujinx/releases/ で Ryujinx をダウンロードしてください.", + "DialogUpdaterDirtyBuildSubMessage": "サポートされているバージョンをお探しなら, https://ryujinx.app/download で Ryujinx をダウンロードしてください.", "DialogRestartRequiredMessage": "再起動が必要", "DialogThemeRestartMessage": "テーマがセーブされました. テーマを適用するには再起動が必要です.", "DialogThemeRestartSubMessage": "再起動しますか", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "ファイル形式のアンインストールに成功しました!", "DialogUninstallFileTypesErrorMessage": "ファイル形式のアンインストールに失敗しました.", "DialogOpenSettingsWindowLabel": "設定ウインドウを開く", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "コントローラアプレット", "DialogMessageDialogErrorExceptionMessage": "メッセージダイアログ表示エラー: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "ソフトウェアキーボード表示エラー: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n続けてよろしいですか?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "ファームウェアをインストール中...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "システムバージョン {0} が正常にインストールされました.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "選択されたプロファイルを削除すると,プロファイルがひとつも存在しなくなります", "DialogUserProfileDeletionConfirmMessage": "選択されたプロファイルを削除しますか", "DialogUserProfileUnsavedChangesTitle": "警告 - 保存されていない変更", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "リストに追加するゲームディレクトリを入力します", "AddGameDirTooltip": "リストにゲームディレクトリを追加します", "RemoveGameDirTooltip": "選択したゲームディレクトリを削除します", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "エミュレータのメニュー外観を変更するためカスタム Avalonia テーマを使用します", "CustomThemePathTooltip": "カスタム GUI テーマのパスです", "CustomThemeBrowseTooltip": "カスタム GUI テーマを参照します", @@ -606,6 +635,8 @@ "DebugLogTooltip": "デバッグログメッセージをコンソールに出力します.\n\nログが読みづらくなり,エミュレータのパフォーマンスが低下するため,開発者から特別な指示がある場合のみ使用してください.", "LoadApplicationFileTooltip": "ロードする Switch 互換のファイルを選択するためファイルエクスプローラを開きます", "LoadApplicationFolderTooltip": "ロードする Switch 互換の展開済みアプリケーションを選択するためファイルエクスプローラを開きます", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "Ryujinx ファイルシステムフォルダを開きます", "OpenRyujinxLogsTooltip": "ログが格納されるフォルダを開きます", "ExitTooltip": "Ryujinx を終了します", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "セットアップガイドを開く", "NoUpdate": "アップデートなし", "TitleUpdateVersionLabel": "バージョン {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - 情報", "RyujinxConfirm": "Ryujinx - 確認", "FileDialogAllTypes": "すべての種別", "Never": "決して", "SwkbdMinCharacters": "最低 {0} 文字必要です", "SwkbdMinRangeCharacters": "{0}-{1} 文字にしてください", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "ソフトウェアキーボード", "SoftwareKeyboardModeNumeric": "0-9 または '.' のみでなければなりません", "SoftwareKeyboardModeAlphabet": "CJK文字以外のみ", @@ -709,16 +751,52 @@ "SelectDlcDialogTitle": "DLC ファイルを選択", "SelectUpdateDialogTitle": "アップデートファイルを選択", "SelectModDialogTitle": "modディレクトリを選択", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "ユーザプロファイルを管理", "CheatWindowTitle": "チート管理", "DlcWindowTitle": "DLC 管理", "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "アップデート管理", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "利用可能なチート {0} [{1}]", "BuildId": "ビルドID:", "DlcWindowHeading": "利用可能な DLC {0} [{1}]", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "編集", + "Continue": "Continue", "Cancel": "キャンセル", "Save": "セーブ", "Discard": "破棄", @@ -767,6 +845,7 @@ "GraphicsScalingFilterBilinear": "Bilinear", "GraphicsScalingFilterNearest": "Nearest", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "レベル", "GraphicsScalingFilterLevelTooltip": "FSR 1.0のシャープ化レベルを設定します. 高い値ほどシャープになります.", "SmaaLow": "SMAA Low", @@ -785,5 +864,17 @@ "MultiplayerMode": "モード:", "MultiplayerModeTooltip": "LDNマルチプレイヤーモードを変更します.\n\nldn_mitmモジュールがインストールされた, 他のRyujinxインスタンスや,ハックされたNintendo Switchコンソールとのローカル/同一ネットワーク接続を可能にします.\n\nマルチプレイでは, すべてのプレイヤーが同じゲームバージョンである必要があります(例:Super Smash Bros. Ultimate v13.0.1はv13.0.0に接続できません).\n\n不明な場合は「無効」のままにしてください.", "MultiplayerModeDisabled": "無効", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index 6e5a7f187..86592aa69 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -1,95 +1,106 @@ { "Language": "한국어", "MenuBarFileOpenApplet": "애플릿 열기", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "독립 실행형 모드로 Mii 편집기 애플릿 열기", - "SettingsTabInputDirectMouseAccess": "다이렉트 마우스 접근", - "SettingsTabSystemMemoryManagerMode": "메모리 관리자 모드:", + "SettingsTabInputDirectMouseAccess": "마우스 직접 접근", + "SettingsTabSystemMemoryManagerMode": "메모리 관리자 모드 :", "SettingsTabSystemMemoryManagerModeSoftware": "소프트웨어", - "SettingsTabSystemMemoryManagerModeHost": "호스트 (빠름)", - "SettingsTabSystemMemoryManagerModeHostUnchecked": "호스트 확인 안함 (가장 빠르나 안전하지 않음)", - "SettingsTabSystemUseHypervisor": "하이퍼바이저 사용하기", - "MenuBarFile": "_파일", - "MenuBarFileOpenFromFile": "_파일에서 응용 프로그램 불러오기", - "MenuBarFileOpenUnpacked": "_압축을 푼 게임 불러오기", + "SettingsTabSystemMemoryManagerModeHost": "호스트(빠름)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "호스트 확인 안함(가장 빠르나 위험)", + "SettingsTabSystemUseHypervisor": "하이퍼바이저 사용", + "MenuBarFile": "파일(_F)", + "MenuBarFileOpenFromFile": "파일에서 앱 불러오기(_L)", + "MenuBarFileOpenFromFileError": "선택한 파일에서 앱을 찾을 수 없습니다.", + "MenuBarFileOpenUnpacked": "압축 푼 게임 불러오기(_U)", + "MenuBarFileLoadDlcFromFolder": "폴더에서 DLC 불러오기", + "MenuBarFileLoadTitleUpdatesFromFolder": "폴더에서 타이틀 업데이트 불러오기", "MenuBarFileOpenEmuFolder": "Ryujinx 폴더 열기", "MenuBarFileOpenLogsFolder": "로그 폴더 열기", - "MenuBarFileExit": "_종료", + "MenuBarFileExit": "종료(_E)", "MenuBarOptions": "옵션(_O)", - "MenuBarOptionsToggleFullscreen": "전체화면 전환", - "MenuBarOptionsStartGamesInFullscreen": "전체 화면 모드에서 게임 시작", + "MenuBarOptionsToggleFullscreen": "전체 화면 전환", + "MenuBarOptionsStartGamesInFullscreen": "전체 화면 모드로 게임 시작", "MenuBarOptionsStopEmulation": "에뮬레이션 중지", - "MenuBarOptionsSettings": "_설정", - "MenuBarOptionsManageUserProfiles": "_사용자 프로파일 관리", - "MenuBarActions": "_동작", - "MenuBarOptionsSimulateWakeUpMessage": "깨우기 메시지 시뮬레이션", + "MenuBarOptionsSettings": "설정(_S)", + "MenuBarOptionsManageUserProfiles": "사용자 프로필 관리(_M)", + "MenuBarActions": "동작(_A)", + "MenuBarOptionsSimulateWakeUpMessage": "웨이크업 메시지 시뮬레이션", "MenuBarActionsScanAmiibo": "Amiibo 스캔", - "MenuBarTools": "_도구", + "MenuBarTools": "도구(_T)", "MenuBarToolsInstallFirmware": "펌웨어 설치", - "MenuBarFileToolsInstallFirmwareFromFile": "XCI 또는 ZIP에서 펌웨어 설치", + "MenuBarFileToolsInstallFirmwareFromFile": "XCI 또는 ZIP으로 펌웨어 설치", "MenuBarFileToolsInstallFirmwareFromDirectory": "디렉터리에서 펌웨어 설치", + "MenuBarToolsInstallKeys": "설치 키", + "MenuBarFileToolsInstallKeysFromFile": "키나 ZIP에서 키 설치", + "MenuBarFileToolsInstallKeysFromFolder": "디렉터리에서 키 설치", "MenuBarToolsManageFileTypes": "파일 형식 관리", "MenuBarToolsInstallFileTypes": "파일 형식 설치", - "MenuBarToolsUninstallFileTypes": "파일 형식 설치 제거", - "MenuBarView": "_보기", - "MenuBarViewWindow": "창 크기", + "MenuBarToolsUninstallFileTypes": "파일 형식 제거", + "MenuBarToolsXCITrimmer": "XCI 파일 트리머", + "MenuBarView": "보기(_V)", + "MenuBarViewWindow": "윈도 창", "MenuBarViewWindow720": "720p", "MenuBarViewWindow1080": "1080p", "MenuBarHelp": "도움말(_H)", "MenuBarHelpCheckForUpdates": "업데이트 확인", "MenuBarHelpAbout": "정보", - "MenuSearch": "검색...", + "MenuSearch": "찾기...", "GameListHeaderFavorite": "즐겨찾기", "GameListHeaderIcon": "아이콘", "GameListHeaderApplication": "이름", "GameListHeaderDeveloper": "개발자", "GameListHeaderVersion": "버전", - "GameListHeaderTimePlayed": "플레이 시간", + "GameListHeaderTimePlayed": "플레이 타임", "GameListHeaderLastPlayed": "마지막 플레이", "GameListHeaderFileExtension": "파일 확장자", "GameListHeaderFileSize": "파일 크기", "GameListHeaderPath": "경로", "GameListContextMenuOpenUserSaveDirectory": "사용자 저장 디렉터리 열기", - "GameListContextMenuOpenUserSaveDirectoryToolTip": "응용프로그램의 사용자 저장이 포함된 디렉터리 열기", - "GameListContextMenuOpenDeviceSaveDirectory": "사용자 장치 디렉토리 열기", - "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "응용프로그램의 장치 저장이 포함된 디렉터리 열기", + "GameListContextMenuOpenUserSaveDirectoryToolTip": "앱의 사용자 저장이 포함된 디렉터리 열기", + "GameListContextMenuOpenDeviceSaveDirectory": "기기 저장 디렉터리 열기", + "GameListContextMenuOpenDeviceSaveDirectoryToolTip": "앱의 장치 저장이 포함된 디렉터리 열기", "GameListContextMenuOpenBcatSaveDirectory": "BCAT 저장 디렉터리 열기", - "GameListContextMenuOpenBcatSaveDirectoryToolTip": "응용프로그램의 BCAT 저장이 포함된 디렉터리 열기", + "GameListContextMenuOpenBcatSaveDirectoryToolTip": "앱의 BCAT 저장이 포함된 디렉터리 열기", "GameListContextMenuManageTitleUpdates": "타이틀 업데이트 관리", "GameListContextMenuManageTitleUpdatesToolTip": "타이틀 업데이트 관리 창 열기", "GameListContextMenuManageDlc": "DLC 관리", "GameListContextMenuManageDlcToolTip": "DLC 관리 창 열기", "GameListContextMenuCacheManagement": "캐시 관리", "GameListContextMenuCacheManagementPurgePptc": "대기열 PPTC 재구성", - "GameListContextMenuCacheManagementPurgePptcToolTip": "다음 게임 시작에서 부팅 시 PPTC가 다시 빌드하도록 트리거", - "GameListContextMenuCacheManagementPurgeShaderCache": "셰이더 캐시 제거", - "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "응용프로그램 셰이더 캐시 삭제\n", + "GameListContextMenuCacheManagementPurgePptcToolTip": "다음 게임 실행 부팅 시, PPTC를 트리거하여 다시 구성", + "GameListContextMenuCacheManagementPurgeShaderCache": "퍼지 셰이더 캐시", + "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "앱의 셰이더 캐시 삭제", "GameListContextMenuCacheManagementOpenPptcDirectory": "PPTC 디렉터리 열기", - "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "응용프로그램 PPTC 캐시가 포함된 디렉터리 열기", + "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "앱의 PPTC 캐시가 포함된 디렉터리 열기", "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "셰이더 캐시 디렉터리 열기", - "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "응용프로그램 셰이더 캐시가 포함된 디렉터리 열기", + "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "앱의 셰이더 캐시가 포함된 디렉터리 열기", "GameListContextMenuExtractData": "데이터 추출", "GameListContextMenuExtractDataExeFS": "ExeFS", - "GameListContextMenuExtractDataExeFSToolTip": "응용프로그램의 현재 구성에서 ExeFS 추출 (업데이트 포함)", + "GameListContextMenuExtractDataExeFSToolTip": "앱의 현재 구성에서 ExeFS 추출(업데이트 포함)", "GameListContextMenuExtractDataRomFS": "RomFS", - "GameListContextMenuExtractDataRomFSToolTip": "응용 프로그램의 현재 구성에서 RomFS 추출 (업데이트 포함)", + "GameListContextMenuExtractDataRomFSToolTip": "앱의 현재 구성에서 RomFS 추출(업데이트 포함)", "GameListContextMenuExtractDataLogo": "로고", - "GameListContextMenuExtractDataLogoToolTip": "응용프로그램의 현재 구성에서 로고 섹션 추출 (업데이트 포함)", - "GameListContextMenuCreateShortcut": "애플리케이션 바로 가기 만들기", - "GameListContextMenuCreateShortcutToolTip": "선택한 애플리케이션을 실행하는 바탕 화면 바로 가기를 만듭니다.", - "GameListContextMenuCreateShortcutToolTipMacOS": "해당 게임을 실행할 수 있는 바로가기를 macOS의 응용 프로그램 폴더에 추가합니다.", - "GameListContextMenuOpenModsDirectory": "Mod 디렉터리 열기", - "GameListContextMenuOpenModsDirectoryToolTip": "해당 게임의 Mod가 저장된 디렉터리 열기", - "GameListContextMenuOpenSdModsDirectory": "Atmosphere Mod 디렉터리 열기", - "GameListContextMenuOpenSdModsDirectoryToolTip": "해당 게임의 Mod가 포함된 대체 SD 카드 Atmosphere 디렉터리를 엽니다. 실제 하드웨어용으로 패키징된 Mod에 유용합니다.", + "GameListContextMenuExtractDataLogoToolTip": "앱의 현재 구성에서 로고 섹션 추출 (업데이트 포함)", + "GameListContextMenuCreateShortcut": "바로 가기 만들기", + "GameListContextMenuCreateShortcutToolTip": "선택한 앱을 실행하는 바탕 화면에 바로 가기를 생성", + "GameListContextMenuCreateShortcutToolTipMacOS": "선택한 앱을 실행하는 macOS 앱 폴더에 바로 가기 만들기", + "GameListContextMenuOpenModsDirectory": "모드 디렉터리 열기", + "GameListContextMenuOpenModsDirectoryToolTip": "앱의 모드가 포함된 디렉터리 열기", + "GameListContextMenuOpenSdModsDirectory": "Atmosphere 모드 디렉터리 열기", + "GameListContextMenuOpenSdModsDirectoryToolTip": "해당 게임의 모드가 포함된 대체 SD 카드 Atmosphere 디렉터리를 엽니다. 실제 하드웨어용으로 패키징된 모드에 유용합니다.", + "GameListContextMenuTrimXCI": "XCI 파일 확인 및 트림", + "GameListContextMenuTrimXCIToolTip": "디스크 공간을 절약하기 위해 XCI 파일 확인 및 트림", "StatusBarGamesLoaded": "{0}/{1}개의 게임 불러옴", "StatusBarSystemVersion": "시스템 버전 : {0}", - "LinuxVmMaxMapCountDialogTitle": "감지된 메모리 매핑의 하한선", + "StatusBarXCIFileTrimming": "XCI 파일 '{0}' 트리밍", + "LinuxVmMaxMapCountDialogTitle": "메모리 매핑 한계 감지", "LinuxVmMaxMapCountDialogTextPrimary": "vm.max_map_count의 값을 {0}으로 늘리시겠습니까?", - "LinuxVmMaxMapCountDialogTextSecondary": "일부 게임은 현재 허용된 것보다 더 많은 메모리 매핑을 생성하려고 시도할 수 있습니다. 이 제한을 초과하는 즉시 Ryujinx에 문제가 발생합니다.", + "LinuxVmMaxMapCountDialogTextSecondary": "일부 게임은 현재 허용된 것보다 더 많은 메모리 매핑을 만들려고 할 수 있습니다. 이 제한을 초과하면 Ryujinx가 충돌이 발생할 수 있습니다.", "LinuxVmMaxMapCountDialogButtonUntilRestart": "예, 다음에 다시 시작할 때까지", "LinuxVmMaxMapCountDialogButtonPersistent": "예, 영구적으로", - "LinuxVmMaxMapCountWarningTextPrimary": "메모리 매핑의 최대 용량이 권장 용량보다 적습니다.", - "LinuxVmMaxMapCountWarningTextSecondary": "vm.max_map_count({0})의 현재 값이 {1}보다 낮습니다. 일부 게임은 현재 허용된 것보다 더 많은 메모리 매핑을 생성하려고 시도할 수 있습니다. 이 제한을 초과하는 즉시 Ryujinx에 문제가 발생합니다.\n\n수동으로 제한을 늘리거나 Ryujinx의 도움을 받을 수 있는 pkexec을 설치하는 것이 좋습니다.", + "LinuxVmMaxMapCountWarningTextPrimary": "메모리 매핑의 최대 용량이 권장 용량보다 부족합니다.", + "LinuxVmMaxMapCountWarningTextSecondary": "vm.max_map_count({0})의 현재 값은 {1}보다 낮습니다. 일부 게임은 현재 허용된 것보다 더 많은 메모리 매핑을 만들려고 할 수 있습니다. Ryujinx는 이 제한을 초과하자마자 충돌할 것입니다.\n\n제한을 수동으로 늘리거나 Ryujinx가 이를 지원할 수 있도록 pkexec를 설치하는 것을 추천합니다.", "Settings": "설정", "SettingsTabGeneral": "사용자 인터페이스", "SettingsTabGeneralGeneral": "일반", @@ -97,17 +108,19 @@ "SettingsTabGeneralCheckUpdatesOnLaunch": "시작 시, 업데이트 확인", "SettingsTabGeneralShowConfirmExitDialog": "\"종료 확인\" 대화 상자 표시", "SettingsTabGeneralRememberWindowState": "창 크기/위치 기억", - "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", - "SettingsTabGeneralHideCursor": "마우스 커서 숨기기", + "SettingsTabGeneralShowTitleBar": "제목 표시줄 표시(다시 시작해야 함)", + "SettingsTabGeneralHideCursor": "커서 숨기기 :", "SettingsTabGeneralHideCursorNever": "절대 안 함", "SettingsTabGeneralHideCursorOnIdle": "유휴 상태", - "SettingsTabGeneralHideCursorAlways": "언제나", - "SettingsTabGeneralGameDirectories": "게임 디렉터리", + "SettingsTabGeneralHideCursorAlways": "항상", + "SettingsTabGeneralGameDirectories": "게임 데릭터리", + "SettingsTabGeneralAutoloadDirectories": "DLC/업데이트 디렉터리 자동 불러오기", + "SettingsTabGeneralAutoloadNote": "누락된 파일을 참조하는 DLC 및 업데이트가 자동으로 언로드", "SettingsTabGeneralAdd": "추가", "SettingsTabGeneralRemove": "제거", "SettingsTabSystem": "시스템", "SettingsTabSystemCore": "코어", - "SettingsTabSystemSystemRegion": "시스템 지역:", + "SettingsTabSystemSystemRegion": "시스템 지역 :", "SettingsTabSystemSystemRegionJapan": "일본", "SettingsTabSystemSystemRegionUSA": "미국", "SettingsTabSystemSystemRegionEurope": "유럽", @@ -117,7 +130,7 @@ "SettingsTabSystemSystemRegionTaiwan": "대만", "SettingsTabSystemSystemLanguage": "시스템 언어 :", "SettingsTabSystemSystemLanguageJapanese": "일본어", - "SettingsTabSystemSystemLanguageAmericanEnglish": "영어(미국)", + "SettingsTabSystemSystemLanguageAmericanEnglish": "미국 영어", "SettingsTabSystemSystemLanguageFrench": "프랑스어", "SettingsTabSystemSystemLanguageGerman": "독일어", "SettingsTabSystemSystemLanguageItalian": "이탈리아어", @@ -128,29 +141,29 @@ "SettingsTabSystemSystemLanguagePortuguese": "포르투갈어", "SettingsTabSystemSystemLanguageRussian": "러시아어", "SettingsTabSystemSystemLanguageTaiwanese": "대만어", - "SettingsTabSystemSystemLanguageBritishEnglish": "영어(영국)", - "SettingsTabSystemSystemLanguageCanadianFrench": "프랑스어(캐나다)", - "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "스페인어(라틴 아메리카)", + "SettingsTabSystemSystemLanguageBritishEnglish": "영국 영어", + "SettingsTabSystemSystemLanguageCanadianFrench": "캐나다 프랑스어", + "SettingsTabSystemSystemLanguageLatinAmericanSpanish": "남미 스페인어", "SettingsTabSystemSystemLanguageSimplifiedChinese": "중국어 간체", "SettingsTabSystemSystemLanguageTraditionalChinese": "중국어 번체", - "SettingsTabSystemSystemTimeZone": "시스템 시간대:", - "SettingsTabSystemSystemTime": "시스템 시간:", + "SettingsTabSystemSystemTimeZone": "시스템 시간대 :", + "SettingsTabSystemSystemTime": "시스템 시간 :", "SettingsTabSystemEnableVsync": "수직 동기화", "SettingsTabSystemEnablePptc": "PPTC(프로파일된 영구 번역 캐시)", - "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableLowPowerPptc": "저전력 PPTC 캐시", "SettingsTabSystemEnableFsIntegrityChecks": "파일 시스템 무결성 검사", "SettingsTabSystemAudioBackend": "음향 후단부 :", "SettingsTabSystemAudioBackendDummy": "더미", "SettingsTabSystemAudioBackendOpenAL": "OpenAL", - "SettingsTabSystemAudioBackendSoundIO": "사운드IO", + "SettingsTabSystemAudioBackendSoundIO": "SoundIO", "SettingsTabSystemAudioBackendSDL2": "SDL2", - "SettingsTabSystemHacks": "해킹", + "SettingsTabSystemHacks": "핵", "SettingsTabSystemHacksNote": "불안정성을 유발할 수 있음", - "SettingsTabSystemDramSize": "대체 메모리 레이아웃 사용(개발자)", - "SettingsTabSystemDramSize4GiB": "4GiB", - "SettingsTabSystemDramSize6GiB": "6GiB", - "SettingsTabSystemDramSize8GiB": "8GiB", - "SettingsTabSystemDramSize12GiB": "12GiB", + "SettingsTabSystemDramSize": "DRAM 크기 :", + "SettingsTabSystemDramSize4GiB": "4GB", + "SettingsTabSystemDramSize6GiB": "6GB", + "SettingsTabSystemDramSize8GiB": "8GB", + "SettingsTabSystemDramSize12GiB": "12GB", "SettingsTabSystemIgnoreMissingServices": "누락된 서비스 무시", "SettingsTabSystemIgnoreApplet": "애플릿 무시", "SettingsTabGraphics": "그래픽", @@ -167,38 +180,38 @@ "SettingsTabGraphicsResolutionScaleNative": "원본(720p/1080p)", "SettingsTabGraphicsResolutionScale2x": "2배(1440p/2160p)", "SettingsTabGraphicsResolutionScale3x": "3배(2160p/3240p)", - "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (권장하지 않음)", + "SettingsTabGraphicsResolutionScale4x": "4배(2880p/4320p) (권장하지 않음)", "SettingsTabGraphicsAspectRatio": "종횡비 :", "SettingsTabGraphicsAspectRatio4x3": "4:3", "SettingsTabGraphicsAspectRatio16x9": "16:9", "SettingsTabGraphicsAspectRatio16x10": "16:10", "SettingsTabGraphicsAspectRatio21x9": "21:9", "SettingsTabGraphicsAspectRatio32x9": "32:9", - "SettingsTabGraphicsAspectRatioStretch": "창에 맞게 늘리기", + "SettingsTabGraphicsAspectRatioStretch": "창에 맞춰 늘리기", "SettingsTabGraphicsDeveloperOptions": "개발자 옵션", "SettingsTabGraphicsShaderDumpPath": "그래픽 셰이더 덤프 경로 :", "SettingsTabLogging": "로그 기록", "SettingsTabLoggingLogging": "로그 기록", "SettingsTabLoggingEnableLoggingToFile": "파일에 로그 기록 활성화", - "SettingsTabLoggingEnableStubLogs": "스텁 로그 활성화", - "SettingsTabLoggingEnableInfoLogs": "정보 로그 활성화", - "SettingsTabLoggingEnableWarningLogs": "경고 로그 활성화", - "SettingsTabLoggingEnableErrorLogs": "오류 로그 활성화", - "SettingsTabLoggingEnableTraceLogs": "추적 로그 활성화", - "SettingsTabLoggingEnableGuestLogs": "게스트 로그 활성화", - "SettingsTabLoggingEnableFsAccessLogs": "Fs 접속 로그 활성화", - "SettingsTabLoggingFsGlobalAccessLogMode": "Fs 전역 접속 로그 모드 :", + "SettingsTabLoggingEnableStubLogs": "조각 기록 활성화", + "SettingsTabLoggingEnableInfoLogs": "정보 기록 활성화", + "SettingsTabLoggingEnableWarningLogs": "경고 기록 활성화", + "SettingsTabLoggingEnableErrorLogs": "오류 기록 활성화", + "SettingsTabLoggingEnableTraceLogs": "추적 기록 활성화", + "SettingsTabLoggingEnableGuestLogs": "방문 기록 활성화", + "SettingsTabLoggingEnableFsAccessLogs": "파일 시스템 접속 기록 활성화", + "SettingsTabLoggingFsGlobalAccessLogMode": "파일 시스템 전역 접속 로그 모드 :", "SettingsTabLoggingDeveloperOptions": "개발자 옵션", - "SettingsTabLoggingDeveloperOptionsNote": "경고: 성능이 저하됨", - "SettingsTabLoggingGraphicsBackendLogLevel": "그래픽 후단부 로그 수준 :", + "SettingsTabLoggingDeveloperOptionsNote": "경고 : 성능이 감소합니다.", + "SettingsTabLoggingGraphicsBackendLogLevel": "그래픽 후단부 기록 레벨 :", "SettingsTabLoggingGraphicsBackendLogLevelNone": "없음", "SettingsTabLoggingGraphicsBackendLogLevelError": "오류", - "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "느려짐", + "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "감속", "SettingsTabLoggingGraphicsBackendLogLevelAll": "모두", - "SettingsTabLoggingEnableDebugLogs": "디버그 로그 활성화", + "SettingsTabLoggingEnableDebugLogs": "디버그 기록 활성화", "SettingsTabInput": "입력", "SettingsTabInputEnableDockedMode": "도킹 모드", - "SettingsTabInputDirectKeyboardAccess": "직접 키보드 접속", + "SettingsTabInputDirectKeyboardAccess": "키보드 직접 접속", "SettingsButtonSave": "저장", "SettingsButtonClose": "닫기", "SettingsButtonOk": "확인", @@ -213,12 +226,12 @@ "ControllerSettingsPlayer6": "플레이어 6", "ControllerSettingsPlayer7": "플레이어 7", "ControllerSettingsPlayer8": "플레이어 8", - "ControllerSettingsHandheld": "휴대 모드", + "ControllerSettingsHandheld": "휴대", "ControllerSettingsInputDevice": "입력 장치", "ControllerSettingsRefresh": "새로 고침", "ControllerSettingsDeviceDisabled": "비활성화됨", "ControllerSettingsControllerType": "컨트롤러 유형", - "ControllerSettingsControllerTypeHandheld": "휴대 모드", + "ControllerSettingsControllerTypeHandheld": "휴대용", "ControllerSettingsControllerTypeProController": "프로 컨트롤러", "ControllerSettingsControllerTypeJoyConPair": "조이콘 페어링", "ControllerSettingsControllerTypeJoyConLeft": "좌측 조이콘", @@ -235,7 +248,7 @@ "ControllerSettingsButtonY": "Y", "ControllerSettingsButtonPlus": "+", "ControllerSettingsButtonMinus": "-", - "ControllerSettingsDPad": "방향 패드", + "ControllerSettingsDPad": "방향키", "ControllerSettingsDPadUp": "↑", "ControllerSettingsDPadDown": "↓", "ControllerSettingsDPadLeft": "←", @@ -245,17 +258,17 @@ "ControllerSettingsStickDown": "↓", "ControllerSettingsStickLeft": "←", "ControllerSettingsStickRight": "→", - "ControllerSettingsStickStick": "스틱", - "ControllerSettingsStickInvertXAxis": "스틱 X 축 반전", - "ControllerSettingsStickInvertYAxis": "스틱 Y 축 반전", - "ControllerSettingsStickDeadzone": "사각지대 :", + "ControllerSettingsStickStick": "스틴", + "ControllerSettingsStickInvertXAxis": "스틱 X축 반전", + "ControllerSettingsStickInvertYAxis": "스틱 Y축 반전", + "ControllerSettingsStickDeadzone": "데드존 :", "ControllerSettingsLStick": "좌측 스틱", "ControllerSettingsRStick": "우측 스틱", "ControllerSettingsTriggersLeft": "좌측 트리거", "ControllerSettingsTriggersRight": "우측 트리거", "ControllerSettingsTriggersButtonsLeft": "좌측 트리거 버튼", "ControllerSettingsTriggersButtonsRight": "우측 트리거 버튼", - "ControllerSettingsTriggers": "트리거 버튼", + "ControllerSettingsTriggers": "트리거", "ControllerSettingsTriggerL": "L", "ControllerSettingsTriggerR": "R", "ControllerSettingsTriggerZL": "ZL", @@ -268,50 +281,50 @@ "ControllerSettingsExtraButtonsRight": "우측 버튼", "ControllerSettingsMisc": "기타", "ControllerSettingsTriggerThreshold": "트리거 임계값 :", - "ControllerSettingsMotion": "동작", + "ControllerSettingsMotion": "모션", "ControllerSettingsMotionUseCemuhookCompatibleMotion": "CemuHook 호환 모션 사용", "ControllerSettingsMotionControllerSlot": "컨트롤러 슬롯 :", "ControllerSettingsMotionMirrorInput": "미러 입력", - "ControllerSettingsMotionRightJoyConSlot": "우측 조이콘 슬롯 :", + "ControllerSettingsMotionRightJoyConSlot": "우측 조이콘 슬롯:", "ControllerSettingsMotionServerHost": "서버 호스트 :", "ControllerSettingsMotionGyroSensitivity": "자이로 감도 :", - "ControllerSettingsMotionGyroDeadzone": "자이로 사각지대 :", + "ControllerSettingsMotionGyroDeadzone": "자이로 데드존 :", "ControllerSettingsSave": "저장", "ControllerSettingsClose": "닫기", "KeyUnknown": "알 수 없음", - "KeyShiftLeft": "왼쪽 Shift", - "KeyShiftRight": "오른쪽 Shift", - "KeyControlLeft": "왼쪽 Ctrl", - "KeyMacControlLeft": "왼쪽 ^", - "KeyControlRight": "오른쪽 Ctrl", - "KeyMacControlRight": "오른쪽 ^", - "KeyAltLeft": "왼쪽 Alt", - "KeyMacAltLeft": "왼쪽 ⌥", - "KeyAltRight": "오른쪽 Alt", - "KeyMacAltRight": "오른쪽 ⌥", - "KeyWinLeft": "왼쪽 ⊞", - "KeyMacWinLeft": "왼쪽 ⌘", - "KeyWinRight": "오른쪽 ⊞", - "KeyMacWinRight": "오른쪽 ⌘", + "KeyShiftLeft": "좌측 Shift", + "KeyShiftRight": "우측 Shift", + "KeyControlLeft": "좌측 Ctrl", + "KeyMacControlLeft": "좌측 ⌃", + "KeyControlRight": "우측 Ctrl", + "KeyMacControlRight": "우측 ⌃", + "KeyAltLeft": "좌측 Alt", + "KeyMacAltLeft": "좌측 ⌥", + "KeyAltRight": "우측 Alt", + "KeyMacAltRight": "우측 ⌥", + "KeyWinLeft": "좌측 ⊞", + "KeyMacWinLeft": "좌측 ⌘", + "KeyWinRight": "우측 ⊞", + "KeyMacWinRight": "우측 ⌘", "KeyMenu": "메뉴", "KeyUp": "↑", "KeyDown": "↓", "KeyLeft": "←", "KeyRight": "→", "KeyEnter": "엔터", - "KeyEscape": "이스케이프", + "KeyEscape": "Esc", "KeySpace": "스페이스", "KeyTab": "탭", "KeyBackSpace": "백스페이스", - "KeyInsert": "Ins", - "KeyDelete": "Del", + "KeyInsert": "Insert", + "KeyDelete": "Delete", "KeyPageUp": "Page Up", "KeyPageDown": "Page Down", "KeyHome": "Home", "KeyEnd": "End", "KeyCapsLock": "Caps Lock", "KeyScrollLock": "Scroll Lock", - "KeyPrintScreen": "프린트 스크린", + "KeyPrintScreen": "Print Screen", "KeyPause": "Pause", "KeyNumLock": "Num Lock", "KeyClear": "지우기", @@ -330,7 +343,7 @@ "KeyKeypadSubtract": "키패드 빼기", "KeyKeypadAdd": "키패드 추가", "KeyKeypadDecimal": "숫자 키패드", - "KeyKeypadEnter": "키패드 엔터", + "KeyKeypadEnter": "키패드 입력", "KeyNumber0": "0", "KeyNumber1": "1", "KeyNumber2": "2", @@ -353,9 +366,9 @@ "KeyPeriod": ".", "KeySlash": "/", "KeyBackSlash": "\\", - "KeyUnbound": "바인딩 해제", - "GamepadLeftStick": "L 스틱 버튼", - "GamepadRightStick": "R 스틱 버튼", + "KeyUnbound": "연동 해제", + "GamepadLeftStick": "좌측 스틱 버튼", + "GamepadRightStick": "우측 스틱 버튼", "GamepadLeftShoulder": "좌측 숄더", "GamepadRightShoulder": "우측 숄더", "GamepadLeftTrigger": "좌측 트리거", @@ -366,181 +379,194 @@ "GamepadDpadRight": "→", "GamepadMinus": "-", "GamepadPlus": "+", - "GamepadGuide": "안내", + "GamepadGuide": "가이드", "GamepadMisc1": "기타", "GamepadPaddle1": "패들 1", "GamepadPaddle2": "패들 2", "GamepadPaddle3": "패들 3", "GamepadPaddle4": "패들 4", "GamepadTouchpad": "터치패드", - "GamepadSingleLeftTrigger0": "왼쪽 트리거 0", - "GamepadSingleRightTrigger0": "오른쪽 트리거 0", - "GamepadSingleLeftTrigger1": "왼쪽 트리거 1", - "GamepadSingleRightTrigger1": "오른쪽 트리거 1", + "GamepadSingleLeftTrigger0": "좌측 트리거 0", + "GamepadSingleRightTrigger0": "우측 트리거 0", + "GamepadSingleLeftTrigger1": "좌측 트리거 1", + "GamepadSingleRightTrigger1": "우측 트리거 1", "StickLeft": "좌측 스틱", "StickRight": "우측 스틱", - "UserProfilesSelectedUserProfile": "선택한 사용자 프로필 :", + "UserProfilesSelectedUserProfile": "선택된 사용자 프로필 :", "UserProfilesSaveProfileName": "프로필 이름 저장", "UserProfilesChangeProfileImage": "프로필 이미지 변경", "UserProfilesAvailableUserProfiles": "사용 가능한 사용자 프로필 :", - "UserProfilesAddNewProfile": "프로필 생성", + "UserProfilesAddNewProfile": "프로필 만들기", "UserProfilesDelete": "삭제", "UserProfilesClose": "닫기", - "ProfileNameSelectionWatermark": "닉네임을 입력하세요", + "ProfileNameSelectionWatermark": "별명 선택", "ProfileImageSelectionTitle": "프로필 이미지 선택", - "ProfileImageSelectionHeader": "프로필 이미지 선택", + "ProfileImageSelectionHeader": "프로필 이미지를 선택", "ProfileImageSelectionNote": "사용자 지정 프로필 이미지를 가져오거나 시스템 펌웨어에서 아바타 선택 가능", "ProfileImageSelectionImportImage": "이미지 파일 가져오기", "ProfileImageSelectionSelectAvatar": "펌웨어 아바타 선택", - "InputDialogTitle": "입력 대화상자", + "InputDialogTitle": "대화 상자 입력", "InputDialogOk": "확인", "InputDialogCancel": "취소", + "InputDialogCancelling": "취소하기", + "InputDialogClose": "닫기", "InputDialogAddNewProfileTitle": "프로필 이름 선택", - "InputDialogAddNewProfileHeader": "프로필 이름 입력", + "InputDialogAddNewProfileHeader": "프로필 이름을 입력", "InputDialogAddNewProfileSubtext": "(최대 길이 : {0})", - "AvatarChoose": "선택", + "AvatarChoose": "아바타 선택", "AvatarSetBackgroundColor": "배경색 설정", "AvatarClose": "닫기", "ControllerSettingsLoadProfileToolTip": "프로필 불러오기", + "ControllerSettingsViewProfileToolTip": "프로필 보기", "ControllerSettingsAddProfileToolTip": "프로필 추가", - "ControllerSettingsRemoveProfileToolTip": "프로필 제거", - "ControllerSettingsSaveProfileToolTip": "프로필 저장", - "MenuBarFileToolsTakeScreenshot": "스크린 샷 찍기", + "ControllerSettingsRemoveProfileToolTip": "프로필 삭제", + "ControllerSettingsSaveProfileToolTip": "프로필 추가", + "MenuBarFileToolsTakeScreenshot": "스크린샷 찍기", "MenuBarFileToolsHideUi": "UI 숨기기", - "GameListContextMenuRunApplication": "응용프로그램 실행", + "GameListContextMenuRunApplication": "앱 실행", "GameListContextMenuToggleFavorite": "즐겨찾기 전환", - "GameListContextMenuToggleFavoriteToolTip": "게임 즐겨찾기 상태 전환", - "SettingsTabGeneralTheme": "테마:", - "SettingsTabGeneralThemeDark": "어두운 테마", - "SettingsTabGeneralThemeLight": "밝은 테마", - "ControllerSettingsConfigureGeneral": "구성", + "GameListContextMenuToggleFavoriteToolTip": "게임의 즐겨찾기 상태 전환", + "SettingsTabGeneralTheme": "테마 :", + "SettingsTabGeneralThemeAuto": "자동", + "SettingsTabGeneralThemeDark": "다크", + "SettingsTabGeneralThemeLight": "라이트", + "ControllerSettingsConfigureGeneral": "설정", "ControllerSettingsRumble": "진동", "ControllerSettingsRumbleStrongMultiplier": "강력한 진동 증폭기", "ControllerSettingsRumbleWeakMultiplier": "약한 진동 증폭기", "DialogMessageSaveNotAvailableMessage": "{0} [{1:x16}]에 대한 저장 데이터가 없음", - "DialogMessageSaveNotAvailableCreateSaveMessage": "이 게임에 대한 저장 데이터를 생성하겠습니까?", + "DialogMessageSaveNotAvailableCreateSaveMessage": "이 게임의 저장 데이터를 만들겠습니까?", "DialogConfirmationTitle": "Ryujinx - 확인", "DialogUpdaterTitle": "Ryujinx - 업데이터", "DialogErrorTitle": "Ryujinx - 오류", "DialogWarningTitle": "Ryujinx - 경고", "DialogExitTitle": "Ryujinx - 종료", - "DialogErrorMessage": "Ryujinx 오류 발생", - "DialogExitMessage": "Ryujinx를 종료하겠습니까?", - "DialogExitSubMessage": "저장하지 않은 모든 데이터는 손실됩니다!", - "DialogMessageCreateSaveErrorMessage": "지정된 저장 데이터를 작성하는 중에 오류 발생: {0}", - "DialogMessageFindSaveErrorMessage": "지정된 저장 데이터를 찾는 중에 오류 발생: {0}", - "FolderDialogExtractTitle": "추출할 폴더 선택", - "DialogNcaExtractionMessage": "{1}에서 {0} 섹션을 추출하는 중...", - "DialogNcaExtractionTitle": "Ryujinx - NCA 섹션 추출기", - "DialogNcaExtractionMainNcaNotFoundErrorMessage": "추출 실패하였습니다. 선택한 파일에 기본 NCA가 없습니다.", - "DialogNcaExtractionCheckLogErrorMessage": "추출 실패하였습니다. 자세한 내용은 로그 파일을 읽으세요.", - "DialogNcaExtractionSuccessMessage": "추출이 성공적으로 완료되었습니다.", - "DialogUpdaterConvertFailedMessage": "현재 Ryujinx 버전을 변환하지 못했습니다.", - "DialogUpdaterCancelUpdateMessage": "업데이트 취소 중 입니다!", - "DialogUpdaterAlreadyOnLatestVersionMessage": "이미 최신 버전의 Ryujinx를 사용하고 있습니다!", - "DialogUpdaterFailedToGetVersionMessage": "GitHub 릴리스에서 릴리스 정보를 가져오는 중에 오류가 발생했습니다. 이는 GitHub Actions에서 새 릴리스를 컴파일하는 경우 발생할 수 있습니다. 몇 분 후에 다시 시도하세요.", - "DialogUpdaterConvertFailedGithubMessage": "Github 개정에서 받은 Ryujinx 버전을 변환하지 못했습니다.", - "DialogUpdaterDownloadingMessage": "업데이트 다운로드 중...", + "DialogErrorMessage": "Ryujinx에서 오류 발생", + "DialogExitMessage": "정말 Ryujinx를 닫으시겠습니까?", + "DialogExitSubMessage": "저장되지 않은 모든 데이터는 손실됩니다!", + "DialogMessageCreateSaveErrorMessage": "지정된 저장 데이터를 생성하는 동안 오류가 발생 : {0}", + "DialogMessageFindSaveErrorMessage": "지정된 저장 데이터를 찾는 중 오류가 발생 : {0}", + "FolderDialogExtractTitle": "압축을 풀 폴더를 선택", + "DialogNcaExtractionMessage": "{1}에서 {0} 단면 추출 중...", + "DialogNcaExtractionTitle": "NCA 단면 추출기", + "DialogNcaExtractionMainNcaNotFoundErrorMessage": "추출에 실패했습니다. 선택한 파일에 기본 NCA가 없습니다.", + "DialogNcaExtractionCheckLogErrorMessage": "추출에 실패했습니다. 자세한 내용은 로그 파일을 확인하시기 바랍니다.", + "DialogNcaExtractionSuccessMessage": "성공적으로 추출이 완료되었습니다.", + "DialogUpdaterConvertFailedMessage": "현재 Ryujinx 버전을 변환할 수 없습니다.", + "DialogUpdaterCancelUpdateMessage": "업데이트가 취소되었습니다!", + "DialogUpdaterAlreadyOnLatestVersionMessage": "이미 최신 버전의 Ryujinx를 사용 중입니다!", + "DialogUpdaterFailedToGetVersionMessage": "GitHub에서 릴리스 정보를 검색하는 동안 오류가 발생했습니다. 현재 GitHub Actions에서 새 릴리스를 컴파일하는 중일 때 발생할 수 있습니다. 몇 분 후에 다시 시도해 주세요.", + "DialogUpdaterConvertFailedGithubMessage": "GitHub에서 받은 Ryujinx 버전을 변환하지 못했습니다.", + "DialogUpdaterDownloadingMessage": "업데이트 내려받는 중...", "DialogUpdaterExtractionMessage": "업데이트 추출 중...", - "DialogUpdaterRenamingMessage": "업데이트 이름 바꾸는 중...", + "DialogUpdaterRenamingMessage": "이름 변경 업데이트...", "DialogUpdaterAddingFilesMessage": "새 업데이트 추가 중...", - "DialogUpdaterCompleteMessage": "업데이트를 완료했습니다!", - "DialogUpdaterRestartMessage": "지금 Ryujinx를 다시 시작하겠습니까?", + "DialogUpdaterShowChangelogMessage": "변경 로그 보기", + "DialogUpdaterCompleteMessage": "업데이트가 완료되었습니다!", + "DialogUpdaterRestartMessage": "지금 Ryujinx를 다시 시작하시겠습니까?", "DialogUpdaterNoInternetMessage": "인터넷에 연결되어 있지 않습니다!", - "DialogUpdaterNoInternetSubMessage": "인터넷 연결이 작동하는지 확인하세요!", - "DialogUpdaterDirtyBuildMessage": "Ryujinx의 나쁜 빌드는 업데이트할 수 없습니다!\n", - "DialogUpdaterDirtyBuildSubMessage": "지원되는 버전을 찾고 있다면 https://https://github.com/GreemDev/Ryujinx/releases/에서 Ryujinx를 다운로드하세요.", - "DialogRestartRequiredMessage": "재시작 필요", - "DialogThemeRestartMessage": "테마가 저장되었습니다. 테마를 적용하려면 다시 시작해야 합니다.", - "DialogThemeRestartSubMessage": "다시 시작하겠습니까?", - "DialogFirmwareInstallEmbeddedMessage": "이 게임에 내장된 펌웨어를 설치하겠습니까? (펌웨어 {0})", - "DialogFirmwareInstallEmbeddedSuccessMessage": "설치된 펌웨어가 없지만 Ryujinx가 제공된 게임에서 펌웨어 {0}을(를) 설치할 수 있었습니다.\n이제 에뮬레이터가 시작됩니다.", - "DialogFirmwareNoFirmwareInstalledMessage": "설치된 펌웨어 없음", + "DialogUpdaterNoInternetSubMessage": "인터넷이 제대로 연결되어 있는지 확인하세요!", + "DialogUpdaterDirtyBuildMessage": "Ryujinx의 더티 빌드는 업데이트할 수 없습니다!", + "DialogUpdaterDirtyBuildSubMessage": "지원되는 버전을 찾으신다면 https://ryujinx.app/download 에서 Ryujinx를 내려받으세요.", + "DialogRestartRequiredMessage": "다시 시작 필요", + "DialogThemeRestartMessage": "테마를 저장했습니다. 테마를 적용하려면 다시 시작해야 합니다.", + "DialogThemeRestartSubMessage": "다시 시작하시겠습니까?", + "DialogFirmwareInstallEmbeddedMessage": "이 게임에 포함된 펌웨어를 설치하시겠습니까?(Firmware {0})", + "DialogFirmwareInstallEmbeddedSuccessMessage": "설치된 펌웨어를 찾을 수 없지만 Ryujinx는 제공된 게임에서 펌웨어 {0}을(를) 설치할 수 있습니다.\n이제 에뮬레이터가 시작됩니다.", + "DialogFirmwareNoFirmwareInstalledMessage": "펌웨어가 설치되어 있지 않음", "DialogFirmwareInstalledMessage": "펌웨어 {0}이(가) 설치됨", "DialogInstallFileTypesSuccessMessage": "파일 형식을 성공적으로 설치했습니다!", "DialogInstallFileTypesErrorMessage": "파일 형식을 설치하지 못했습니다.", - "DialogUninstallFileTypesSuccessMessage": "파일 형식을 성공적으로 제거했습니다!", + "DialogUninstallFileTypesSuccessMessage": "파일 형식이 성공적으로 제거되었습니다!", "DialogUninstallFileTypesErrorMessage": "파일 형식을 제거하지 못했습니다.", "DialogOpenSettingsWindowLabel": "설정 창 열기", + "DialogOpenXCITrimmerWindowLabel": "XCI 트리머 창", "DialogControllerAppletTitle": "컨트롤러 애플릿", - "DialogMessageDialogErrorExceptionMessage": "메시지 대화상자를 표시하는 동안 오류 발생 : {0}", - "DialogSoftwareKeyboardErrorExceptionMessage": "소프트웨어 키보드를 표시하는 동안 오류 발생 : {0}", - "DialogErrorAppletErrorExceptionMessage": "오류에플릿 대화상자를 표시하는 동안 오류 발생 : {0}", + "DialogMessageDialogErrorExceptionMessage": "메시지 대화 상자 표시 오류 : {0}", + "DialogSoftwareKeyboardErrorExceptionMessage": "소프트웨어 키보드 표시 오류 : {0}", + "DialogErrorAppletErrorExceptionMessage": "ErrorApplet 대화 상자 표시 오류 : {0}", "DialogUserErrorDialogMessage": "{0}: {1}", - "DialogUserErrorDialogInfoMessage": "\n이 오류를 수정하는 방법에 대한 자세한 내용은 설정 가이드를 따르세요.", - "DialogUserErrorDialogTitle": "Ryuijnx 오류 ({0})", + "DialogUserErrorDialogInfoMessage": "\n이 오류를 해결하는 방법에 대한 자세한 내용은 설정 가이드를 참조하세요.", + "DialogUserErrorDialogTitle": "Ryujinx 오류 ({0})", "DialogAmiiboApiTitle": "Amiibo API", - "DialogAmiiboApiFailFetchMessage": "API에서 정보를 가져오는 동안 오류가 발생했습니다.", - "DialogAmiiboApiConnectErrorMessage": "Amiibo API 서버에 연결할 수 없습니다. 서비스가 다운되었거나 인터넷 연결이 온라인 상태인지 확인해야 할 수 있습니다.", - "DialogProfileInvalidProfileErrorMessage": "{0} 프로필은 현재 입력 구성 시스템과 호환되지 않습니다.", - "DialogProfileDefaultProfileOverwriteErrorMessage": "기본 프로필을 덮어쓸 수 없음", - "DialogProfileDeleteProfileTitle": "프로필 삭제", - "DialogProfileDeleteProfileMessage": "이 작업은 되돌릴 수 없습니다. 계속하겠습니까?", + "DialogAmiiboApiFailFetchMessage": "API에서 정보를 가져오는 중에 오류가 발생했습니다.", + "DialogAmiiboApiConnectErrorMessage": "Amiibo API 서버에 연결할 수 없습니다. 서비스가 다운되었거나 인터넷 연결이 온라인 상태인지 확인이 필요합니다.", + "DialogProfileInvalidProfileErrorMessage": "프로필 {0}은(는) 현재 입력 구성 시스템과 호환되지 않습니다.", + "DialogProfileDefaultProfileOverwriteErrorMessage": "기본 프로필은 덮어쓸 수 없음", + "DialogProfileDeleteProfileTitle": "프로필 삭제하기", + "DialogProfileDeleteProfileMessage": "이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?", "DialogWarning": "경고", - "DialogPPTCDeletionMessage": "다음 부팅 시, PPTC 재구축을 대기열에 추가 :\n\n{0}\n\n계속하겠습니까?", - "DialogPPTCDeletionErrorMessage": "{0}에서 PPTC 캐시 삭제 오류 : {1}", - "DialogShaderDeletionMessage": "다음에 대한 셰이더 캐시 삭제 :\n\n{0}\n\n계속하겠습니까?", - "DialogShaderDeletionErrorMessage": "{0}에서 셰이더 캐시 제거 오류 : {1}", - "DialogRyujinxErrorMessage": "Ryujinx에 오류 발생", + "DialogPPTCDeletionMessage": "다음에 부팅할 때, PPTC 재구축을 대기열에 추가하려고 합니다.\n\n{0}\n\n계속하시겠습니까?", + "DialogPPTCDeletionErrorMessage": "{0}에서 PPTC 캐시를 지우는 중 오류 발생 : {1}", + "DialogShaderDeletionMessage": "다음 셰이더 캐시를 삭제 :\n\n{0}\n\n계속하시겠습니까?", + "DialogShaderDeletionErrorMessage": "{0}에서 셰이더 캐시를 삭제하는 중 오류 발생 : {1}", + "DialogRyujinxErrorMessage": "Ryujinx에서 오류 발생", "DialogInvalidTitleIdErrorMessage": "UI 오류 : 선택한 게임에 유효한 타이틀 ID가 없음", "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "{0}에서 유효한 시스템 펌웨어를 찾을 수 없습니다.", "DialogFirmwareInstallerFirmwareInstallTitle": "펌웨어 {0} 설치", "DialogFirmwareInstallerFirmwareInstallMessage": "시스템 버전 {0}이(가) 설치됩니다.", - "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n이것은 현재 시스템 버전 {0}을(를) 대체합니다.", - "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n계속하겠습니까?", + "DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\n현재 시스템 버전 {0}을(를) 대체합니다.", + "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n계속하시겠습니까?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "펌웨어 설치 중...", - "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "시스템 버전 {0}이(가) 성공적으로 설치되었습니다.", - "DialogUserProfileDeletionWarningMessage": "선택한 프로파일이 삭제되면 사용 가능한 다른 프로파일이 없음", - "DialogUserProfileDeletionConfirmMessage": "선택한 프로파일을 삭제하겠습니까?", - "DialogUserProfileUnsavedChangesTitle": "경고 - 변경사항 저장되지 않음", - "DialogUserProfileUnsavedChangesMessage": "저장되지 않은 사용자 프로파일을 수정했습니다.", - "DialogUserProfileUnsavedChangesSubMessage": "변경사항을 저장하지 않으시겠습니까?", + "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "시스템 버전 {0}이(가) 설치되었습니다.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "{0}에서 잘못된 키 파일이 발견", + "DialogKeysInstallerKeysInstallTitle": "설치 키", + "DialogKeysInstallerKeysInstallMessage": "새로운 키 파일이 설치됩니다.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\n이로 인해 현재 설치된 키 중 일부가 대체될 수 있습니다.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\n계속하시겠습니까?", + "DialogKeysInstallerKeysInstallWaitMessage": "키 설치 중...", + "DialogKeysInstallerKeysInstallSuccessMessage": "새로운 키 파일이 성공적으로 설치되었습니다.", + "DialogUserProfileDeletionWarningMessage": "선택한 프로필을 삭제하면 다른 프로필을 열 수 없음", + "DialogUserProfileDeletionConfirmMessage": "선택한 프로필을 삭제하시겠습니까?", + "DialogUserProfileUnsavedChangesTitle": "경고 - 저장되지 않은 변경 사항", + "DialogUserProfileUnsavedChangesMessage": "저장되지 않은 사용자 프로필의 변경 사항이 있습니다.", + "DialogUserProfileUnsavedChangesSubMessage": "변경 사항을 취소하시겠습니까?", "DialogControllerSettingsModifiedConfirmMessage": "현재 컨트롤러 설정이 업데이트되었습니다.", - "DialogControllerSettingsModifiedConfirmSubMessage": "저장하겠습니까?", - "DialogLoadFileErrorMessage": "{0}. 오류 발생 파일 : {1}", - "DialogModAlreadyExistsMessage": "Mod가 이미 존재합니다.", - "DialogModInvalidMessage": "지정된 디렉터리에 Mod가 없습니다!", - "DialogModDeleteNoParentMessage": "삭제 실패: \"{0}\" Mod의 상위 디렉터리를 찾을 수 없습니다!", - "DialogDlcNoDlcErrorMessage": "지정된 파일에 선택한 타이틀에 대한 DLC가 포함되어 있지 않습니다!", - "DialogPerformanceCheckLoggingEnabledMessage": "개발자만 사용하도록 설계된 추적 로그 기록이 활성화되어 있습니다.", - "DialogPerformanceCheckLoggingEnabledConfirmMessage": "최적의 성능을 위해 추적 로그 생성을 비활성화하는 것이 좋습니다. 지금 추적 로그 기록을 비활성화하겠습니까?", - "DialogPerformanceCheckShaderDumpEnabledMessage": "개발자만 사용하도록 설계된 셰이더 덤프를 활성화했습니다.", - "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "최적의 성능을 위해 세이더 덤핑을 비활성화하는 것이 좋습니다. 지금 세이더 덤핑을 비활성화하겠습니까?", - "DialogLoadAppGameAlreadyLoadedMessage": "이미 게임 불러옴", - "DialogLoadAppGameAlreadyLoadedSubMessage": "다른 게임을 시작하기 전에 에뮬레이션을 중지하거나 에뮬레이터를 닫으세요.", - "DialogUpdateAddUpdateErrorMessage": "지정된 파일에 선택한 제목에 대한 업데이트가 포함되어 있지 않습니다!", + "DialogControllerSettingsModifiedConfirmSubMessage": "저장하시겠습니까?", + "DialogLoadFileErrorMessage": "{0}. 오류 파일 : {1}", + "DialogModAlreadyExistsMessage": "이미 존재하는 모드", + "DialogModInvalidMessage": "지정한 디렉터리에 모드가 없습니다!", + "DialogModDeleteNoParentMessage": "삭제 실패 : \"{0}\" 모드의 상위 디렉터리를 찾을 수 없습니다!", + "DialogDlcNoDlcErrorMessage": "지정된 파일에 선택한 타이틀의 DLC가 포함되어 있지 않습니다!", + "DialogPerformanceCheckLoggingEnabledMessage": "개발자만 사용하도록 설계된 추적 기록이 활성화되어 있습니다.", + "DialogPerformanceCheckLoggingEnabledConfirmMessage": "최적의 성능을 위해서는 추적 기록을 비활성화하는 것이 좋습니다. 지금 추적 기록을 비활성화하시겠습니까?", + "DialogPerformanceCheckShaderDumpEnabledMessage": "개발자만 사용하도록 설계된 셰이더 덤핑이 활성화되어 있습니다.", + "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "최적의 성능을 위해서는 셰이더 덤핑을 비활성화하는 것이 좋습니다. 지금 셰이더 덤핑을 비활성화하시겠습니까?", + "DialogLoadAppGameAlreadyLoadedMessage": "이미 게임을 불러옴", + "DialogLoadAppGameAlreadyLoadedSubMessage": "다른 게임을 실행하기 전에 에뮬레이션을 중지하거나 에뮬레이터를 닫으세요.", + "DialogUpdateAddUpdateErrorMessage": "지정한 파일에 선택한 타이틀에 대한 업데이트가 포함되어 있지 않습니다!", "DialogSettingsBackendThreadingWarningTitle": "경고 - 후단부 스레딩", - "DialogSettingsBackendThreadingWarningMessage": "변경 사항을 완전히 적용하려면 이 옵션을 변경한 후, Ryujinx를 다시 시작해야 합니다. 플랫폼에 따라 Ryujinx를 사용할 때 드라이버 자체의 멀티스레딩을 수동으로 비활성화해야 할 수도 있습니다.", - "DialogModManagerDeletionWarningMessage": "해당 Mod를 삭제하려고 합니다: {0}\n\n정말로 삭제하시겠습니까?", - "DialogModManagerDeletionAllWarningMessage": "해당 타이틀에 대한 모든 Mod들을 삭제하려고 합니다.\n\n정말로 삭제하시겠습니까?", + "DialogSettingsBackendThreadingWarningMessage": "완전히 적용하려면 이 옵션을 변경한 후 Ryujinx를 다시 시작해야 합니다. 플랫폼에 따라 Ryujinx를 사용할 때 드라이버 자체의 다중 스레딩을 수동으로 비활성화해야 할 수도 있습니다.", + "DialogModManagerDeletionWarningMessage": "모드 삭제 : {0}\n\n계속하시겠습니까?", + "DialogModManagerDeletionAllWarningMessage": "이 타이틀에 대한 모드를 모두 삭제하려고 합니다.\n\n계속하시겠습니까?", "SettingsTabGraphicsFeaturesOptions": "기능", - "SettingsTabGraphicsBackendMultithreading": "그래픽 후단부 멀티스레딩 :", + "SettingsTabGraphicsBackendMultithreading": "그래픽 후단부 다중 스레딩 :", "CommonAuto": "자동", "CommonOff": "끔", "CommonOn": "켬", "InputDialogYes": "예", "InputDialogNo": "아니오", "DialogProfileInvalidProfileNameErrorMessage": "파일 이름에 잘못된 문자가 포함되어 있습니다. 다시 시도하세요.", - "MenuBarOptionsPauseEmulation": "일시 정지", + "MenuBarOptionsPauseEmulation": "일시 중지", "MenuBarOptionsResumeEmulation": "다시 시작", - "AboutUrlTooltipMessage": "기본 브라우저에서 Ryujinx 웹사이트를 열려면 클릭하세요.", - "AboutDisclaimerMessage": "Ryujinx는 닌텐도™,\n또는 그 파트너와 제휴한 바가 없습니다.", - "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com)는\nAmiibo 에뮬레이션에 사용됩니다.", - "AboutPatreonUrlTooltipMessage": "기본 브라우저에서 Ryujinx Patreon 페이지를 열려면 클릭하세요.", - "AboutGithubUrlTooltipMessage": "기본 브라우저에서 Ryujinx GitHub 페이지를 열려면 클릭하세요.", - "AboutDiscordUrlTooltipMessage": "기본 브라우저에서 Ryujinx 디스코드 서버에 대한 초대를 열려면 클릭하세요.", - "AboutTwitterUrlTooltipMessage": "기본 브라우저에서 Ryujinx 트위터 페이지를 열려면 클릭하세요.", + "AboutUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx 웹사이트가 열립니다.", + "AboutDisclaimerMessage": "Ryujinx는 Nintendo™\n또는 그 파트너와 제휴한 바가 없습니다.", + "AboutAmiiboDisclaimerMessage": "AmiiboAPI(www.amiiboapi.com)는\nAmiibo 에뮬레이션에 사용됩니다.", + "AboutPatreonUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx Patreon 페이지가 열립니다.", + "AboutGithubUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx GitHub 페이지가 열립니다.", + "AboutDiscordUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx 디스코드 서버 초대장이 열립니다.", + "AboutTwitterUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx 트위터 페이지가 열립니다.", "AboutRyujinxAboutTitle": "정보 :", - "AboutRyujinxAboutContent": "Ryujinx는 닌텐도 스위치™용 에뮬레이터입니다.\nPatreon에서 지원해 주세요.\n트위터나 디스코드에서 최신 소식을 받아보세요.\n기여에 참여하고자 하는 개발자는 GitHub 또는 디스코드에서 자세한 내용을 확인할 수 있습니다.", + "AboutRyujinxAboutContent": "Ryujinx는 Nintendo Switch™용 에뮬레이터입니다.\nPatreon에서 저희를 후원해 주세요.\nTwitter나 Discord에서 최신 뉴스를 모두 받아보세요.\n기여에 관심이 있는 개발자는 GitHub이나 Discord에서 자세한 내용을 알아볼 수 있습니다.", "AboutRyujinxMaintainersTitle": "유지 관리 :", - "AboutRyujinxMaintainersContentTooltipMessage": "기본 브라우저에서 기여자 페이지를 열려면 클릭하세요.", - "AboutRyujinxSupprtersTitle": "Patreon에서 후원:", + "AboutRyujinxMaintainersContentTooltipMessage": "클릭하면 기본 브라우저에서 기여자 페이지가 열립니다.", + "AboutRyujinxSupprtersTitle": "Patreon에서 후원 :", "AmiiboSeriesLabel": "Amiibo 시리즈", "AmiiboCharacterLabel": "캐릭터", - "AmiiboScanButtonLabel": "스캔", + "AmiiboScanButtonLabel": "스캔하기", "AmiiboOptionsShowAllLabel": "모든 Amiibo 표시", - "AmiiboOptionsUsRandomTagLabel": "해킹: 임의의 태그 UUID 사용", - "DlcManagerTableHeadingEnabledLabel": "활성화됨", + "AmiiboOptionsUsRandomTagLabel": "핵 : 무작위 태그 Uuid 사용", + "DlcManagerTableHeadingEnabledLabel": "활성화", "DlcManagerTableHeadingTitleIdLabel": "타이틀 ID", "DlcManagerTableHeadingContainerPathLabel": "컨테이너 경로", "DlcManagerTableHeadingFullPathLabel": "전체 경로", @@ -549,145 +575,161 @@ "DlcManagerDisableAllButton": "모두 비활성화", "ModManagerDeleteAllButton": "모두 삭제", "MenuBarOptionsChangeLanguage": "언어 변경", - "MenuBarShowFileTypes": "파일 유형 표시", + "MenuBarShowFileTypes": "파일 형식 표시", "CommonSort": "정렬", "CommonShowNames": "이름 표시", "CommonFavorite": "즐겨찾기", "OrderAscending": "오름차순", "OrderDescending": "내림차순", - "SettingsTabGraphicsFeatures": "기능ㆍ개선 사항", + "SettingsTabGraphicsFeatures": "기능 및 개선 사항", "ErrorWindowTitle": "오류 창", - "ToggleDiscordTooltip": "\"현재 재생 중인\" 디스코드 활동에 Ryujinx를 표시할지 여부 선택", - "AddGameDirBoxTooltip": "목록에 추가할 게임 디렉터리 입력", + "ToggleDiscordTooltip": "\"현재 진행 중인\" 디스코드 활동에 Ryujinx를 표시할지 여부를 선택", + "AddGameDirBoxTooltip": "목록에 추가할 게임 디렉터리를 입력", "AddGameDirTooltip": "목록에 게임 디렉터리 추가", "RemoveGameDirTooltip": "선택한 게임 디렉터리 제거", - "CustomThemeCheckTooltip": "GUI에 사용자 지정 Avalonia 테마를 사용하여 에뮬레이터 메뉴의 모양 변경", + "AddAutoloadDirBoxTooltip": "목록에 추가할 자동 불러오기 디렉터리를 입력", + "AddAutoloadDirTooltip": "목록에 자동 불러오기 디렉터리 추가", + "RemoveAutoloadDirTooltip": "선택한 자동 불러오기 디렉터리 제거", + "CustomThemeCheckTooltip": "GUI용 사용자 정의 Avalonia 테마를 사용하여 에뮬레이터 메뉴의 모양 변경", "CustomThemePathTooltip": "사용자 정의 GUI 테마 경로", "CustomThemeBrowseTooltip": "사용자 정의 GUI 테마 찾아보기", - "DockModeToggleTooltip": "독 모드에서는 에뮬레이트된 시스템이 도킹된 닌텐도 스위치처럼 작동합니다. 이것은 대부분의 게임에서 그래픽 품질을 향상시킵니다. 반대로 이 기능을 비활성화하면 에뮬레이트된 시스템이 휴대용 닌텐도 스위치처럼 작동하여 그래픽 품질이 저하됩니다.\n\n독 모드를 사용하려는 경우 플레이어 1의 컨트롤을 구성하세요. 휴대 모드를 사용하려는 경우 휴대용 컨트롤을 구성하세요.\n\n확실하지 않으면 켜 두세요.", - "DirectKeyboardTooltip": "다이렉트 키보드 접근(HID)은 게임에서 사용자의 키보드를 텍스트 입력 장치로 사용할 수 있게끔 제공합니다.\n\n스위치 하드웨어에서 키보드 사용을 네이티브로 지원하는 게임에서만 작동합니다.\n\n이 옵션에 대해 잘 모른다면 끄기를 권장합니다.", - "DirectMouseTooltip": "다이렉트 마우스 접근(HID)은 게임에서 사용자의 마우스를 포인터 장치로 사용할 수 있게끔 제공합니다.\n\n스위치 하드웨어에서 마우스 사용을 네이티브로 지원하는 극히 일부 게임에서만 작동합니다.\n\n이 옵션이 활성화된 경우, 터치 스크린 기능이 작동하지 않을 수 있습니다.\n\n이 옵션에 대해 잘 모른다면 끄기를 권장합니다.", + "DockModeToggleTooltip": "도킹 모드를 사용하면 에뮬레이트된 시스템이 도킹된 Nintendo Switch처럼 동작합니다. 이 경우, 대부분의 게임에서 그래픽 충실도를 향상시킵니다. 반대로 이 기능을 비활성화하면 에뮬레이트된 시스템이 휴대용 Nintendo Switch처럼 작동하여 그래픽 품질이 저하됩니다.\n\n도킹 모드를 사용할 계획이라면 플레이어 1 컨트롤을 구성하세요. 휴대용 모드를 사용하려는 경우 휴대용 컨트롤을 구성하십시오.\n\n모르면 켬으로 두세요.", + "DirectKeyboardTooltip": "키보드 직접 접속(HID)을 지원합니다. 텍스트 입력 장치로 키보드에 대한 게임 접속을 제공합니다.\n\nSwitch 하드웨어에서 키보드 사용을 기본적으로 지원하는 게임에서만 작동합니다.\n\n모르면 끔으로 두세요.", + "DirectMouseTooltip": "마우스 직접 접속(HID)을 지원합니다. 마우스에 대한 게임 접속을 포인팅 장치로 제공합니다.\n\nSwitch 하드웨어에서 마우스 컨트롤을 기본적으로 지원하는 게임에서만 작동하며 거의 없습니다.\n\n활성화하면 터치 스크린 기능이 작동하지 않을 수 있습니다.\n\n모르면 끔으로 두세요.", "RegionTooltip": "시스템 지역 변경", "LanguageTooltip": "시스템 언어 변경", "TimezoneTooltip": "시스템 시간대 변경", "TimeTooltip": "시스템 시간 변경", - "VSyncToggleTooltip": "에뮬레이트된 콘솔의 수직 동기화. 기본적으로 대부분의 게임에 대한 프레임 제한 장치로, 비활성화시 게임이 더 빠른 속도로 실행되거나 로딩 화면이 더 오래 걸리거나 멈출 수 있습니다.\n\n게임 내에서 선호하는 핫키로 전환할 수 있습니다(기본값 F1). 핫키를 비활성화할 계획이라면 이 작업을 수행하는 것이 좋습니다.\n\n이 옵션에 대해 잘 모른다면 켜기를 권장드립니다.", - "PptcToggleTooltip": "게임이 불러올 때마다 번역할 필요가 없도록 번역된 JIT 기능을 저장합니다.\n\n게임을 처음 부팅한 후 끊김 현상을 줄이고 부팅 시간을 크게 단축합니다.\n\n확실하지 않으면 켜 두세요.", - "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", - "FsIntegrityToggleTooltip": "게임을 부팅할 때 손상된 파일을 확인하고 손상된 파일이 감지되면 로그에 해시 오류를 표시합니다.\n\n성능에 영향을 미치지 않으며 문제 해결에 도움이 됩니다.\n\n확실하지 않으면 켜 두세요.", - "AudioBackendTooltip": "오디오를 렌더링하는 데 사용되는 백엔드를 변경합니다.\n\nSDL2가 선호되는 반면 OpenAL 및 사운드IO는 폴백으로 사용됩니다. 더미는 소리가 나지 않습니다.\n\n확실하지 않으면 SDL2로 설정하세요.", - "MemoryManagerTooltip": "게스트 메모리가 매핑되고 접속되는 방식을 변경합니다. 에뮬레이트된 CPU 성능에 크게 영향을 미칩니다.\n\n확실하지 않은 경우 호스트 확인 안함으로 설정하세요.", - "MemoryManagerSoftwareTooltip": "주소 변환을 위해 소프트웨어 페이지 테이블을 사용하세요. 정확도는 가장 높지만 성능은 가장 느립니다.", - "MemoryManagerHostTooltip": "호스트 주소 공간의 메모리를 직접 매핑합니다. 훨씬 빠른 JIT 컴파일 및 실행합니다.", - "MemoryManagerUnsafeTooltip": "메모리를 직접 매핑하지만 접속하기 전에 게스트 주소 공간 내의 주소를 마스킹하지 마십시오. 더 빠르지만 안전을 희생해야 합니다. 게스트 응용 프로그램은 Ryujinx의 어디에서나 메모리에 접속할 수 있으므로 이 모드에서는 신뢰할 수 있는 프로그램만 실행하세요.", - "UseHypervisorTooltip": "JIT 대신 하이퍼바이저를 사용합니다. 하이퍼바이저를 사용할 수 있을 때 성능을 향상시키지만, 현재 상태에서는 불안정할 수 있습니다.", - "DRamTooltip": "대체 메모리모드 레이아웃을 활용하여 스위치 개발 모델을 모방합니다.\n\n고해상도 텍스처 팩 또는 4k 해상도 모드에만 유용합니다. 성능을 향상시키지 않습니다.\n\n확실하지 않으면 꺼 두세요.", - "IgnoreMissingServicesTooltip": "구현되지 않은 호라이즌 OS 서비스를 무시합니다. 이것은 특정 게임을 부팅할 때 충돌을 우회하는 데 도움이 될 수 있습니다.\n\n확실하지 않으면 꺼 두세요.", - "IgnoreAppletTooltip": "게임 플레이 중에 게임패드의 연결이 끊어지면 외부 대화 상자 '컨트롤러 애플릿'이 나타나지 않습니다. 대화 상자를 닫거나 새 컨트롤러를 설정하라는 메시지도 표시되지 않습니다. 이전에 연결이 끊어진 컨트롤러가 다시 연결되면 게임이 자동으로 재개됩니다.", - "GraphicsBackendThreadingTooltip": "두 번째 스레드에서 그래픽 백엔드 명령을 실행합니다.\n\n세이더 컴파일 속도를 높이고 끊김 현상을 줄이며 자체 멀티스레딩 지원 없이 GPU 드라이버의 성능을 향상시킵니다. 멀티스레딩이 있는 드라이버에서 성능이 약간 향상되었습니다.\n\n잘 모르겠으면 자동으로 설정하세요.", - "GalThreadingTooltip": "두 번째 스레드에서 그래픽 백엔드 명령을 실행합니다.\n\n세이더 컴파일 속도를 높이고 끊김 현상을 줄이며 자체 멀티스레딩 지원 없이 GPU 드라이버의 성능을 향상시킵니다. 멀티스레딩이 있는 드라이버에서 성능이 약간 향상되었습니다.\n\n잘 모르겠으면 자동으로 설정하세요.", - "ShaderCacheToggleTooltip": "후속 실행에서 끊김 현상을 줄이는 디스크 세이더 캐시를 저장합니다.\n\n확실하지 않으면 켜 두세요.", - "ResolutionScaleTooltip": "게임의 렌더링 해상도를 늘립니다.\n\n일부 게임에서는 해당 기능을 지원하지 않거나 해상도가 늘어났음에도 픽셀이 자글자글해 보일 수 있습니다; 이러한 게임들의 경우 사용자가 직접 안티 앨리어싱 기능을 끄는 Mod나 내부 렌더링 해상도를 증가시키는 Mod 등을 찾아보아야 합니다. 후자의 Mod를 사용 시에는 해당 옵션을 네이티브로 두시는 것이 좋습니다.\n\n이 옵션은 게임이 구동중일 때에도 아래 Apply 버튼을 눌러서 변경할 수 있습니다; 설정 창을 게임 창 옆에 두고 사용자가 선호하는 해상도를 실험하여 고를 수 있습니다.\n\n4x 설정은 어떤 셋업에서도 무리인 점을 유의하세요.", - "ResolutionScaleEntryTooltip": "1.5와 같은 부동 소수점 분해능 스케일입니다. 비통합 척도는 문제나 충돌을 일으킬 가능성이 더 큽니다.", - "AnisotropyTooltip": "비등방성 필터링 레벨. 게임에서 요청한 값을 사용하려면 자동으로 설정하세요.", - "AspectRatioTooltip": "렌더러 창에 적용될 화면비.\n\n화면비를 변경하는 Mod를 사용할 때에만 이 옵션을 바꾸세요, 그렇지 않을 경우 그래픽이 늘어나 보일 수 있습니다.\n\n이 옵션에 대해 잘 모른다면 16:9로 설정하세요.", + "VSyncToggleTooltip": "에뮬레이트된 콘솔의 수직 동기화입니다. 기본적으로 대부분의 게임에서 프레임 제한 기능으로, 비활성화하면 게임이 더 빠른 속도로 실행되거나 로딩 화면이 더 오래 걸리거나 멈출 수 있습니다.\n\n게임 내에서 원하는 단축키(기본값은 F1)로 전환할 수 있습니다. 비활성화하려면 이 작업을 수행하는 것이 좋습니다.\n\n모르면 켬으로 두세요.", + "PptcToggleTooltip": "번역된 JIT 함수를 저장하여 게임을 불러올 때마다 번역할 필요가 없도록 합니다.\n\n게임을 처음 부팅한 후 끊김 현상을 줄이고 부팅 시간을 크게 단축합니다.\n\n모르면 켬으로 두세요.", + "LowPowerPptcToggleTooltip": "코어의 3분의 1을 사용하여 PPTC를 불러옵니다.", + "FsIntegrityToggleTooltip": "게임을 부팅할 때 손상된 파일을 확인하고, 손상된 파일이 감지되면 로그에 해시 오류를 표시합니다.\n\n성능에 영향을 미치지 않으며 문제 해결에 도움이 됩니다.\n\n모르면 켬으로 두세요.", + "AudioBackendTooltip": "오디오 렌더링에 사용되는 백엔드를 변경합니다.\n\nSDL2가 선호되는 반면 OpenAL 및 SoundIO는 대체 수단으로 사용됩니다. 더미에는 소리가 나지 않습니다.\n\n모르면 SDL2로 설정하세요.", + "MemoryManagerTooltip": "게스트 메모리 매핑 및 접속 방법을 변경합니다. 에뮬레이트된 CPU 성능에 큰 영향을 미칩니다.\n\n모르면 호스트 확인 안 함으로 설정합니다.", + "MemoryManagerSoftwareTooltip": "주소 번역에 소프트웨어 페이지 테이블을 사용합니다. 정확도는 가장 높지만 가장 느립니다.", + "MemoryManagerHostTooltip": "호스트 주소 공간에 메모리를 직접 매핑합니다. JIT 컴파일 및 실행 속도가 훨씬 빨라집니다.", + "MemoryManagerUnsafeTooltip": "메모리를 직접 매핑하되 접속하기 전에 게스트 주소 공간 내의 주소를 마스킹하지 않습니다. 더 빠르지만 안전성이 희생됩니다. 게스트 애플리케이션은 Ryujinx의 어느 곳에서나 메모리에 접속할 수 있으므로 이 모드에서는 신뢰할 수 있는 프로그램만 실행하세요.", + "UseHypervisorTooltip": "JIT 대신 Hypervisor를 사용하세요. 사용 가능한 경우 성능이 크게 향상되지만 현재 상태에서는 불안정할 수 있습니다.", + "DRamTooltip": "Switch 개발 모델을 모방하기 위해 8GB DRAM이 포함된 대체 메모리 모드를 활용합니다.\n\n이는 고해상도 텍스처 팩 또는 4K 해상도 모드에만 유용합니다. 성능을 개선하지 않습니다.\n\n모르면 끔으로 두세요.", + "IgnoreMissingServicesTooltip": "구현되지 않은 Horizon OS 서비스는 무시됩니다. 특정 게임을 부팅할 때, 발생하는 충돌을 우회하는 데 도움이 될 수 있습니다.\n\n모르면 끔으로 두세요.", + "IgnoreAppletTooltip": "게임 플레이 중에 게임패드 연결이 끊어지면 외부 대화 상자 \"컨트롤러 애플릿\"이 나타나지 않습니다. 대화 상자를 닫거나 새 컨트롤러를 설정하라는 메시지가 표시되지 않습니다. 이전에 연결이 끊어진 컨트롤러가 다시 연결되면 게임이 자동으로 다시 시작됩니다.", + "GraphicsBackendThreadingTooltip": "2번째 스레드에서 그래픽 후단부 명령을 실행합니다.\n\n셰이더 컴파일 속도를 높이고, 끊김 현상을 줄이며, 자체 다중 스레딩 지원 없이 GPU 드라이버의 성능을 향상시킵니다. 다중 스레딩이 있는 드라이버에서 성능이 좀 더 좋습니다.\n\n모르면 자동으로 설정합니다.", + "GalThreadingTooltip": "2번째 스레드에서 그래픽 후단부 명령을 실행합니다.\n\n셰이더 컴파일 속도를 높이고 끊김 현상을 줄이며 자체 다중 스레딩 지원 없이 GPU 드라이버의 성능을 향상시킵니다. 다중 스레딩이 있는 드라이버에서 성능이 좀 더 좋습니다.\n\n모르면 자동으로 설정합니다.", + "ShaderCacheToggleTooltip": "후속 실행 시 끊김 현상을 줄이는 디스크 셰이더 캐시를 저장합니다.\n\n모르면 켬으로 두세요.", + "ResolutionScaleTooltip": "게임의 렌더링 해상도를 배가시킵니다.\n\n일부 게임에서는 이 기능이 작동하지 않고 해상도가 높아져도 픽셀화되어 보일 수 있습니다. 해당 게임의 경우 앤티 앨리어싱을 제거하거나 내부 렌더링 해상도를 높이는 모드를 찾아야 할 수 있습니다. 후자를 사용하려면 기본을 선택하는 것이 좋습니다.\n\n이 옵션은 아래의 \"적용\"을 클릭하여 게임이 실행되는 동안 변경할 수 있습니다. 설정 창을 옆으로 옮기고 원하는 게임 모양을 찾을 때까지 실험해 보세요.\n\n4배는 거의 모든 설정에서 과하다는 점을 명심하세요.", + "ResolutionScaleEntryTooltip": "부동 소수점 해상도 스케일(예: 1.5)입니다. 적분이 아닌 스케일은 문제나 충돌을 일으킬 가능성이 높습니다.", + "AnisotropyTooltip": "이방성 필터링 수준입니다. 게임에서 요청한 값을 사용하려면 자동으로 설정하세요.", + "AspectRatioTooltip": "렌더러 창에 적용되는 종횡비입니다.\n\n게임에 종횡비 모드를 사용하는 경우에만 이 설정을 변경하세요. 그렇지 않으면 그래픽이 늘어납니다.\n\n모르면 16:9로 두세요.", "ShaderDumpPathTooltip": "그래픽 셰이더 덤프 경로", - "FileLogTooltip": "디스크의 로그 파일에 콘솔 로깅을 저장합니다. 성능에 영향을 미치지 않습니다.", - "StubLogTooltip": "콘솔에 스텁 로그 메시지를 인쇄합니다. 성능에 영향을 미치지 않습니다.", - "InfoLogTooltip": "콘솔에 정보 로그 메시지를 인쇄합니다. 성능에 영향을 미치지 않습니다.", - "WarnLogTooltip": "콘솔에 경고 로그 메시지를 인쇄합니다. 성능에 영향을 미치지 않습니다.", - "ErrorLogTooltip": "콘솔에 오류 로그 메시지를 인쇄합니다. 성능에 영향을 미치지 않습니다.", - "TraceLogTooltip": "콘솔에 추적 로그 메시지를 인쇄합니다. 성능에 영향을 미치지 않습니다.", - "GuestLogTooltip": "콘솔에 게스트 로그 메시지를 인쇄합니다. 성능에 영향을 미치지 않습니다.", - "FileAccessLogTooltip": "콘솔에 파일 액세스 로그 메시지를 인쇄합니다.", - "FSAccessLogModeTooltip": "콘솔에 대한 FS 접속 로그 출력을 활성화합니다. 가능한 모드는 0-3\t\t\t\t", + "FileLogTooltip": "디스크의 로그 파일에 콘솔 기록을 저장합니다. 성능에 영향을 주지 않습니다.", + "StubLogTooltip": "콘솔에 조각 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "InfoLogTooltip": "콘솔에 정보 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "WarnLogTooltip": "콘솔에 경고 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "ErrorLogTooltip": "콘솔에 오류 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "TraceLogTooltip": "콘솔에 추적 기록 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "GuestLogTooltip": "콘솔에 게스트 로그 메시지를 출력합니다. 성능에 영향을 주지 않습니다.", + "FileAccessLogTooltip": "콘솔에 파일 접속 기록 메시지를 출력합니다.", + "FSAccessLogModeTooltip": "콘솔에 파일 시스템 접속 기록 출력을 활성화합니다. 가능한 모드는 0-3", "DeveloperOptionTooltip": "주의해서 사용", - "OpenGlLogLevel": "적절한 로그 수준을 활성화해야 함", - "DebugLogTooltip": "콘솔에 디버그 로그 메시지를 인쇄합니다.\n\n로그를 읽기 어렵게 만들고 에뮬레이터 성능을 악화시키므로 직원이 구체적으로 지시한 경우에만 사용하세요.", - "LoadApplicationFileTooltip": "파일 탐색기를 열어 불러올 스위치 호환 파일 선택", - "LoadApplicationFolderTooltip": "파일 탐색기를 열어 불러올 스위치 호환 압축 해제 응용 프로그램 선택", + "OpenGlLogLevel": "적절한 기록 수준이 활성화되어 있어야 함", + "DebugLogTooltip": "콘솔에 디버그 기록 메시지를 출력합니다.\n\n담당자가 특별히 요청한 경우에만 이 기능을 사용하십시오. 로그를 읽기 어렵게 만들고 에뮬레이터 성능을 저하시킬 수 있기 때문입니다.", + "LoadApplicationFileTooltip": "파일 탐색기를 열어 불러올 Switch 호환 파일을 선택", + "LoadApplicationFolderTooltip": "Switch와 호환되는 압축 해제된 앱을 선택하여 불러오려면 파일 탐색기를 엽니다.", + "LoadDlcFromFolderTooltip": "파일 탐색기를 열어 DLC를 일괄 불러오기할 폴더를 하나 이상 선택", + "LoadTitleUpdatesFromFolderTooltip": "파일 탐색기를 열어 하나 이상의 폴더를 선택하여 대량으로 타이틀 업데이트 불러오기", "OpenRyujinxFolderTooltip": "Ryujinx 파일 시스템 폴더 열기", - "OpenRyujinxLogsTooltip": "로그가 기록된 폴더 열기", + "OpenRyujinxLogsTooltip": "로그가 기록되는 폴더 열기", "ExitTooltip": "Ryujinx 종료", "OpenSettingsTooltip": "설정 창 열기", - "OpenProfileManagerTooltip": "사용자 프로파일 관리자 창 열기", - "StopEmulationTooltip": "현재 게임의 에뮬레이션을 중지하고 게임 선택으로 돌아감", + "OpenProfileManagerTooltip": "사용자 프로필 관리자 창 열기", + "StopEmulationTooltip": "현재 게임의 에뮬레이션을 중지하고 게임 선택으로 돌아가기", "CheckUpdatesTooltip": "Ryujinx 업데이트 확인", "OpenAboutTooltip": "정보 창 열기", - "GridSize": "격자 크기", - "GridSizeTooltip": "격자 항목의 크기 변경", - "SettingsTabSystemSystemLanguageBrazilianPortuguese": "포르투갈어(브라질)", + "GridSize": "그리드 크기", + "GridSizeTooltip": "그리드 항목의 크기 변경", + "SettingsTabSystemSystemLanguageBrazilianPortuguese": "브라질 포르투갈어", "AboutRyujinxContributorsButtonHeader": "모든 기여자 보기", "SettingsTabSystemAudioVolume": "음량 : ", - "AudioVolumeTooltip": "음향 음량 변경", + "AudioVolumeTooltip": "음량 변경", "SettingsTabSystemEnableInternetAccess": "게스트 인터넷 접속/LAN 모드", - "EnableInternetAccessTooltip": "에뮬레이션된 응용프로그램이 인터넷에 연결되도록 허용합니다.\n\nLAN 모드가 있는 게임은 이 모드가 활성화되고 시스템이 동일한 접속 포인트에 연결된 경우 서로 연결할 수 있습니다. 여기에는 실제 콘솔도 포함됩니다.\n\n닌텐도 서버에 연결할 수 없습니다. 인터넷에 연결을 시도하는 특정 게임에서 충돌이 발생할 수 있습니다.\n\n확실하지 않으면 꺼두세요.", + "EnableInternetAccessTooltip": "에뮬레이트된 앱을 인터넷에 연결할 수 있습니다.\n\nLAN 모드가 있는 게임은 이 기능이 활성화되고 시스템이 동일한 접속 포인트에 연결되어 있을 때 서로 연결할 수 있습니다. 이는 실제 콘솔도 포함됩니다.\n\nNintendo 서버 연결을 허용하지 않습니다. 인터넷에 연결을 시도하는 특정 게임에서 충돌이 발생할 수 있습니다.\n\n모르면 끔으로 두세요.", "GameListContextMenuManageCheatToolTip": "치트 관리", "GameListContextMenuManageCheat": "치트 관리", - "GameListContextMenuManageModToolTip": "Mod 관리", - "GameListContextMenuManageMod": "Mod 관리", + "GameListContextMenuManageModToolTip": "모드 관리", + "GameListContextMenuManageMod": "모드 관리", "ControllerSettingsStickRange": "범위 :", "DialogStopEmulationTitle": "Ryujinx - 에뮬레이션 중지", - "DialogStopEmulationMessage": "에뮬레이션을 중지하겠습니까?", + "DialogStopEmulationMessage": "에뮬레이션을 중지하시겠습니까?", "SettingsTabCpu": "CPU", - "SettingsTabAudio": "오디오", + "SettingsTabAudio": "음향", "SettingsTabNetwork": "네트워크", "SettingsTabNetworkConnection": "네트워크 연결", "SettingsTabCpuCache": "CPU 캐시", "SettingsTabCpuMemory": "CPU 모드", "DialogUpdaterFlatpakNotSupportedMessage": "FlatHub를 통해 Ryujinx를 업데이트하세요.", - "UpdaterDisabledWarningTitle": "업데이터 비활성화입니다!", + "UpdaterDisabledWarningTitle": "업데이터가 비활성화되었습니다!", "ControllerSettingsRotate90": "시계 방향으로 90° 회전", "IconSize": "아이콘 크기", "IconSizeTooltip": "게임 아이콘 크기 변경", "MenuBarOptionsShowConsole": "콘솔 표시", - "ShaderCachePurgeError": "{0}에서 셰이더 캐시를 제거하는 중 오류 발생: {1}", + "ShaderCachePurgeError": "{0}에서 셰이더 캐시를 삭제하는 중 오류 발생 : {1}", "UserErrorNoKeys": "키를 찾을 수 없음", "UserErrorNoFirmware": "펌웨어를 찾을 수 없음", "UserErrorFirmwareParsingFailed": "펌웨어 구문 분석 오류", - "UserErrorApplicationNotFound": "응용 프로그램을 찾을 수 없음", + "UserErrorApplicationNotFound": "앱을 찾을 수 없음", "UserErrorUnknown": "알 수 없는 오류", "UserErrorUndefined": "정의되지 않은 오류", - "UserErrorNoKeysDescription": "Ryujinx가 'prod.keys' 파일을 찾을 수 없음", - "UserErrorNoFirmwareDescription": "Ryujinx가 설치된 펌웨어를 찾을 수 없음", - "UserErrorFirmwareParsingFailedDescription": "Ryujinx가 제공된 펌웨어를 구문 분석할 수 없습니다. 일반적으로 오래된 키가 원인입니다.", - "UserErrorApplicationNotFoundDescription": "Ryujinx가 지정된 경로에서 유효한 응용 프로그램을 찾을 수 없습니다.", + "UserErrorNoKeysDescription": "Ryujinx가 'prod.keys' 파일을 찾지 못함", + "UserErrorNoFirmwareDescription": "설치된 펌웨어를 찾을 수 없음", + "UserErrorFirmwareParsingFailedDescription": "Ryujinx가 제공된 펌웨어를 구문 분석하지 못했습니다. 이는 일반적으로 오래된 키로 인해 발생합니다.", + "UserErrorApplicationNotFoundDescription": "Ryujinx가 해당 경로에서 유효한 앱을 찾을 수 없습니다.", "UserErrorUnknownDescription": "알 수 없는 오류가 발생했습니다!", - "UserErrorUndefinedDescription": "정의되지 않은 오류가 발생했습니다! 이런 일이 발생하면 안 되므로, 개발자에게 문의하세요!", + "UserErrorUndefinedDescription": "정의되지 않은 오류가 발생했습니다! 이런 일이 발생하면 안 되니 개발자에게 문의하세요!", "OpenSetupGuideMessage": "설정 가이드 열기", "NoUpdate": "업데이트 없음", "TitleUpdateVersionLabel": "버전 {0}", + "TitleBundledUpdateVersionLabel": "번들 : 버전 {0}", + "TitleBundledDlcLabel": "번들 :", + "TitleXCIStatusPartialLabel": "일부", + "TitleXCIStatusTrimmableLabel": "트리밍되지 않음", + "TitleXCIStatusUntrimmableLabel": "트리밍됨", + "TitleXCIStatusFailedLabel": "(실패)", + "TitleXCICanSaveLabel": "{0:n0} Mb 저장", + "TitleXCISavingLabel": "{0:n0}Mb 저장됨", "RyujinxInfo": "Ryujinx - 정보", "RyujinxConfirm": "Ryujinx - 확인", - "FileDialogAllTypes": "모든 유형", + "FileDialogAllTypes": "모든 형식", "Never": "절대 안 함", "SwkbdMinCharacters": "{0}자 이상이어야 함", - "SwkbdMinRangeCharacters": "{0}-{1}자여야 함", + "SwkbdMinRangeCharacters": "{0}-{1}자 길이여야 함", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "소프트웨어 키보드", - "SoftwareKeyboardModeNumeric": "'0~9' 또는 '.'만 가능", - "SoftwareKeyboardModeAlphabet": "한중일 문자가 아닌 문자만 가능", + "SoftwareKeyboardModeNumeric": "0-9 또는 '.'만 가능", + "SoftwareKeyboardModeAlphabet": "CJK 문자가 아닌 문자만 가능", "SoftwareKeyboardModeASCII": "ASCII 텍스트만 가능", - "ControllerAppletControllers": "지원하는 컨트롤러:", - "ControllerAppletPlayers": "플레이어:", - "ControllerAppletDescription": "현재 설정은 유효하지 않습니다. 설정을 열어 입력 장치를 다시 설정하세요.", - "ControllerAppletDocked": "독 모드가 설정되었습니다. 핸드헬드 컨트롤은 비활성화됩니다.", - "UpdaterRenaming": "이전 파일 이름 바꾸는 중...", - "UpdaterRenameFailed": "업데이터가 파일 이름을 바꿀 수 없음: {0}", - "UpdaterAddingFiles": "새로운 파일을 추가하는 중...", - "UpdaterExtracting": "업데이트를 추출하는 중...", - "UpdaterDownloading": "업데이트 다운로드 중...", + "ControllerAppletControllers": "지원되는 컨트롤러 :", + "ControllerAppletPlayers": "플레이어 :", + "ControllerAppletDescription": "현재 구성이 유효하지 않습니다. 설정을 열고 입력을 다시 구성하십시오.", + "ControllerAppletDocked": "도킹 모드가 설정되었습니다. 휴대용 제어 기능을 비활성화해야 합니다.", + "UpdaterRenaming": "오래된 파일 이름 바꾸기...", + "UpdaterRenameFailed": "업데이터가 파일 이름을 바꿀 수 없음 : {0}", + "UpdaterAddingFiles": "새 파일 추가...", + "UpdaterExtracting": "업데이트 추출...", + "UpdaterDownloading": "업데이트 내려받기 중...", "Game": "게임", - "Docked": "도킹됨", - "Handheld": "휴대용", - "ConnectionError": "연결 오류입니다.", - "AboutPageDeveloperListMore": "{0} 등...", - "ApiError": "API 오류입니다.", - "LoadingHeading": "{0} 로딩 중", - "CompilingPPTC": "PTC 컴파일 중", - "CompilingShaders": "셰이더 컴파일 중", + "Docked": "도킹", + "Handheld": "휴대", + "ConnectionError": "연결 오류가 발생했습니다.", + "AboutPageDeveloperListMore": "{0} 외...", + "ApiError": "API 오류.", + "LoadingHeading": "{0} 불러오는 중", + "CompilingPPTC": "PTC 컴파일", + "CompilingShaders": "셰이더 컴파일", "AllKeyboards": "모든 키보드", - "OpenFileDialogTitle": "지원되는 파일을 선택", - "OpenFolderDialogTitle": "압축을 푼 게임이 있는 폴더 선택", + "OpenFileDialogTitle": "지원되는 파일을 선택하여 열기", + "OpenFolderDialogTitle": "압축 해제된 게임이 있는 폴더를 선택", "AllSupportedFormats": "지원되는 모든 형식", "RyujinxUpdater": "Ryujinx 업데이터", "SettingsTabHotkeys": "키보드 단축키", @@ -695,9 +737,9 @@ "SettingsTabHotkeysToggleVsyncHotkey": "수직 동기화 전환 :", "SettingsTabHotkeysScreenshotHotkey": "스크린샷 :", "SettingsTabHotkeysShowUiHotkey": "UI 표시 :", - "SettingsTabHotkeysPauseHotkey": "일시 중지 :", - "SettingsTabHotkeysToggleMuteHotkey": "음 소거 :", - "ControllerMotionTitle": "동작 제어 설정", + "SettingsTabHotkeysPauseHotkey": "중지 :", + "SettingsTabHotkeysToggleMuteHotkey": "음소거 :", + "ControllerMotionTitle": "모션 컨트롤 설정", "ControllerRumbleTitle": "진동 설정", "SettingsSelectThemeFileDialogTitle": "테마 파일 선택", "SettingsXamlThemeFile": "Xaml 테마 파일", @@ -708,82 +750,132 @@ "Writable": "쓰기 가능", "SelectDlcDialogTitle": "DLC 파일 선택", "SelectUpdateDialogTitle": "업데이트 파일 선택", - "SelectModDialogTitle": "Mod 디렉터리 선택", - "UserProfileWindowTitle": "사용자 프로파일 관리자", + "SelectModDialogTitle": "모드 디렉터리 선택", + "TrimXCIFileDialogTitle": "XCI 파일 확인 및 정리", + "TrimXCIFileDialogPrimaryText": "이 기능은 먼저 충분한 공간을 확보한 다음 XCI 파일을 트리밍하여 디스크 공간을 절약합니다.", + "TrimXCIFileDialogSecondaryText": "현재 파일 크기 : {0:n}MB\n게임 데이터 크기 : {1:n}MB\n디스크 공간 절약 : {2:n}MB", + "TrimXCIFileNoTrimNecessary": "XCI 파일은 트리밍할 필요가 없습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileNoUntrimPossible": "XCI 파일은 트리밍을 해제할 수 없습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileReadOnlyFileCannotFix": "XCI 파일은 읽기 전용이므로 쓰기 가능하게 만들 수 없습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileFileSizeChanged": "XCI 파일이 스캔된 후 크기가 변경되었습니다. 파일이 쓰여지고 있지 않은지 확인하고 다시 시도하세요.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI 파일에 여유 공간 영역에 데이터가 있으므로 트리밍하는 것이 안전하지 않음", + "TrimXCIFileInvalidXCIFile": "XCI 파일에 유효하지 않은 데이터가 포함되어 있습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileFileIOWriteError": "XCI 파일을 쓰기 위해 열 수 없습니다. 자세한 내용은 로그를 확인", + "TrimXCIFileFailedPrimaryText": "XCI 파일 트리밍에 실패", + "TrimXCIFileCancelled": "작업이 취소됨", + "TrimXCIFileFileUndertermined": "작업이 수행되지 않음", + "UserProfileWindowTitle": "사용자 프로필 관리자", "CheatWindowTitle": "치트 관리자", - "DlcWindowTitle": "{0} ({1})의 다운로드 가능한 콘텐츠 관리", - "ModWindowTitle": "{0} ({1})의 Mod 관리", + "DlcWindowTitle": "{0} ({1})의 내려받기 가능한 콘텐츠 관리", + "ModWindowTitle": "{0}({1})의 모드 관리", "UpdateWindowTitle": "타이틀 업데이트 관리자", - "CheatWindowHeading": "{0} [{1}]에 사용할 수 있는 치트", - "BuildId": "빌드ID :", - "DlcWindowHeading": "{0} 내려받기 가능한 콘텐츠", - "ModWindowHeading": "{0} Mod(s)", - "UserProfilesEditProfile": "선택된 항목 편집", + "XCITrimmerWindowTitle": "XCI 파일 트리머", + "XCITrimmerTitleStatusCount": "{1}개 타이틀 중 {0}개 선택됨", + "XCITrimmerTitleStatusCountWithFilter": "{1}개 타이틀 중 {0}개 선택됨({2}개 표시됨)", + "XCITrimmerTitleStatusTrimming": "{0}개의 타이틀을 트리밍 중...", + "XCITrimmerTitleStatusUntrimming": "{0}개의 타이틀을 트리밍 해제 중...", + "XCITrimmerTitleStatusFailed": "실패", + "XCITrimmerPotentialSavings": "잠재적 비용 절감", + "XCITrimmerActualSavings": "실제 비용 절감", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "표시됨 선택", + "XCITrimmerDeselectDisplayed": "표시됨 선택 취소", + "XCITrimmerSortName": "타이틀", + "XCITrimmerSortSaved": "공간 절약s", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0}개의 새 업데이트가 추가됨", + "UpdateWindowBundledContentNotice": "번들 업데이트는 제거할 수 없으며, 비활성화만 가능합니다.", + "CheatWindowHeading": "{0} [{1}]에 사용 가능한 치트", + "BuildId": "빌드ID:", + "DlcWindowBundledContentNotice": "번들 DLC는 제거할 수 없으며 비활성화만 가능합니다.", + "DlcWindowHeading": "{1} ({2})에 내려받기 가능한 콘텐츠 {0}개 사용 가능", + "DlcWindowDlcAddedMessage": "{0}개의 새로운 내려받기 가능한 콘텐츠가 추가됨", + "AutoloadDlcAddedMessage": "{0}개의 새로운 내려받기 가능한 콘텐츠가 추가됨", + "AutoloadDlcRemovedMessage": "{0}개의 내려받기 가능한 콘텐츠가 제거됨", + "AutoloadUpdateAddedMessage": "{0}개의 새 업데이트가 추가됨", + "AutoloadUpdateRemovedMessage": "누락된 업데이트 {0}개 삭제", + "ModWindowHeading": "{0} 모드", + "UserProfilesEditProfile": "선택 항목 편집", + "Continue": "계속", "Cancel": "취소", "Save": "저장", - "Discard": "삭제", - "Paused": "일시 중지", - "UserProfilesSetProfileImage": "프로파일 이미지 설정", - "UserProfileEmptyNameError": "이름 필요", - "UserProfileNoImageError": "프로파일 이미지를 설정해야 함", + "Discard": "폐기", + "Paused": "일시 중지됨", + "UserProfilesSetProfileImage": "프로필 이미지 설정", + "UserProfileEmptyNameError": "이름 필수 입력", + "UserProfileNoImageError": "프로필 이미지를 설정해야 함", "GameUpdateWindowHeading": "{0} ({1})에 대한 업데이트 관리", "SettingsTabHotkeysResScaleUpHotkey": "해상도 증가 :", "SettingsTabHotkeysResScaleDownHotkey": "해상도 감소 :", "UserProfilesName": "이름 :", "UserProfilesUserId": "사용자 ID :", "SettingsTabGraphicsBackend": "그래픽 후단부", - "SettingsTabGraphicsBackendTooltip": "에뮬레이터에 사용될 그래픽 백엔드를 선택합니다.\n\nVulkan이 드라이버가 최신이기 때문에 모든 현대 그래픽 카드들에서 더 좋은 성능을 발휘합니다. 또한 Vulkan은 모든 벤더사의 GPU에서 더 빠른 쉐이더 컴파일을 지원하여 스터터링이 적습니다.\n\nOpenGL의 경우 오래된 Nvidia GPU나 오래된 AMD GPU(리눅스 한정), 혹은 VRAM이 적은 GPU에서 더 나은 성능을 발휘할 수는 있으나 쉐이더 컴파일로 인한 스터터링이 Vulkan보다 심할 수 있습니다.\n\n이 옵션에 대해 잘 모른다면 Vulkan으로 설정하세요. 사용하는 GPU가 최신 그래픽 드라이버에서도 Vulkan을 지원하지 않는다면 그 땐 OpenGL로 설정하세요.", + "SettingsTabGraphicsBackendTooltip": "에뮬레이터에서 사용할 그래픽 후단부를 선택합니다.\n\nVulkan은 드라이버가 최신 상태인 한 모든 최신 그래픽 카드에 전반적으로 더 좋습니다. Vulkan은 또한 모든 GPU 공급업체에서 더 빠른 셰이더 컴파일(덜 끊김)을 제공합니다.\n\nOpenGL은 오래된 Nvidia GPU, Linux의 오래된 AMD GPU 또는 VRAM이 낮은 GPU에서 더 나은 결과를 얻을 수 있지만 셰이더 컴파일 끊김이 더 큽니다.\n\n모르면 Vulkan으로 설정합니다. 최신 그래픽 드라이버를 사용해도 GPU가 Vulkan을 지원하지 않는 경우 OpenGL로 설정하세요..", "SettingsEnableTextureRecompression": "텍스처 재압축 활성화", - "SettingsEnableTextureRecompressionTooltip": "ASTC 텍스처를 압축하여 VRAM 사용량을 줄입니다.\n\n애스트럴 체인, 바요네타 3, 파이어 엠블렘 인게이지, 메트로이드 프라임 리마스터, 슈퍼 마리오브라더스 원더, 젤다의 전설: 티어스 오브 더 킹덤 등이 이러한 텍스처 포맷을 사용합니다.\n\nVRAM이 4GiB 이하인 그래픽 카드로 위와 같은 게임들을 구동할시 특정 지점에서 크래시가 발생할 수 있습니다.\n\n위에 서술된 게임들에서 VRAM이 부족한 경우에만 해당 옵션을 켜고, 그 외의 경우에는 끄기를 권장드립니다.", - "SettingsTabGraphicsPreferredGpu": "선호하는 GPU", - "SettingsTabGraphicsPreferredGpuTooltip": "Vulkan 그래픽 후단부와 함께 사용할 그래픽 카드를 선택하세요.\n\nOpenGL이 사용할 GPU에는 영향을 미치지 않습니다.\n\n확실하지 않은 경우 \"dGPU\" 플래그가 지정된 GPU로 설정하세요. 없는 경우, 그대로 두세요.", + "SettingsEnableTextureRecompressionTooltip": "VRAM 사용량을 줄이기 위해 ASTC 텍스처를 압축합니다.\n\n이 텍스처 형식을 사용하는 게임에는 Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder, The Legend of Zelda: Tears of the Kingdom이 있습니다.\n\n4GiB VRAM 이하의 그래픽 카드는 이러한 게임을 실행하는 동안 어느 시점에서 충돌할 가능성이 있습니다.\n\n위에서 언급한 게임에서 VRAM이 부족한 경우에만 활성화합니다. 모르면 끔으로 두세요.", + "SettingsTabGraphicsPreferredGpu": "기본 GPU", + "SettingsTabGraphicsPreferredGpuTooltip": "Vulkan 그래픽 후단부와 함께 사용할 그래픽 카드를 선택하세요.\n\nOpenGL에서 사용할 GPU에는 영향을 미치지 않습니다.\n\n모르면 \"dGPU\"로 플래그가 지정된 GPU로 설정하세요. 없으면 그대로 두세요.", "SettingsAppRequiredRestartMessage": "Ryujinx 다시 시작 필요", - "SettingsGpuBackendRestartMessage": "그래픽 후단부 또는 GPU 설정이 수정되었습니다. 적용하려면 다시 시작해야 합니다.", - "SettingsGpuBackendRestartSubMessage": "지금 다시 시작하겠습니까?", - "RyujinxUpdaterMessage": "Ryujinx를 최신 버전으로 업데이트하겠습니까?", + "SettingsGpuBackendRestartMessage": "그래픽 후단부 또는 GPU 설정이 수정되었습니다. 이를 적용하려면 다시 시작이 필요", + "SettingsGpuBackendRestartSubMessage": "지금 다시 시작하시겠습니까?", + "RyujinxUpdaterMessage": "Ryujinx를 최신 버전으로 업데이트하시겠습니까?", "SettingsTabHotkeysVolumeUpHotkey": "음량 증가 :", "SettingsTabHotkeysVolumeDownHotkey": "음량 감소 :", "SettingsEnableMacroHLE": "매크로 HLE 활성화", - "SettingsEnableMacroHLETooltip": "GPU 매크로 코드의 높은 수준 에뮬레이션입니다.\n\n성능이 향상되지만 일부 게임에서 그래픽 결함이 발생할 수 있습니다.\n\n확실하지 않으면 켜 두세요.", + "SettingsEnableMacroHLETooltip": "GPU 매크로 코드의 고수준 에뮬레이션입니다.\n\n성능은 향상되지만 일부 게임에서 그래픽 오류가 발생할 수 있습니다.\n\n모르면 켬으로 두세요.", "SettingsEnableColorSpacePassthrough": "색 공간 통과", - "SettingsEnableColorSpacePassthroughTooltip": "색 공간을 지정하지 않고 색상 정보를 전달하도록 Vulkan 후단에 지시합니다. 와이드 가멋 디스플레이를 사용하는 사용자의 경우 색 정확도가 저하되지만 더 생생한 색상을 얻을 수 있습니다.", + "SettingsEnableColorSpacePassthroughTooltip": "Vulkan 후단부가 색 공간을 지정하지 않고 색상 정보를 전달하도록 지시합니다. 넓은 색역 화면 표시 장치를 사용하는 사용자의 경우 색상 정확성을 희생하고 더 생생한 색상이 나올 수 있습니다.", "VolumeShort": "음량", "UserProfilesManageSaves": "저장 관리", - "DeleteUserSave": "이 게임에 대한 사용자 저장을 삭제하겠습니까?", + "DeleteUserSave": "이 게임의 사용자 저장을 삭제하시겠습니까?", "IrreversibleActionNote": "이 작업은 되돌릴 수 없습니다.", - "SaveManagerHeading": "{0} ({1})의 저장 관리", - "SaveManagerTitle": "저장 관리자", + "SaveManagerHeading": "{0} ({1})에 대한 저장 관리", + "SaveManagerTitle": "관리자 저장", "Name": "이름", "Size": "크기", - "Search": "검색", + "Search": "찾기", "UserProfilesRecoverLostAccounts": "잃어버린 계정 복구", "Recover": "복구", "UserProfilesRecoverHeading": "다음 계정에 대한 저장 발견", - "UserProfilesRecoverEmptyList": "복구할 프로파일이 없습니다", - "GraphicsAATooltip": "게임 렌더에 안티 앨리어싱을 적용합니다.\n\nFXAA는 대부분의 이미지를 뿌옇게 만들지만, SMAA는 들쭉날쭉한 모서리 부분들을 찾아 부드럽게 만듭니다.\n\nFSR 스케일링 필터와 같이 사용하는 것은 권장하지 않습니다.\n\n이 옵션은 게임이 구동중일 때에도 아래 Apply 버튼을 눌러서 변경할 수 있습니다; 설정 창을 게임 창 옆에 두고 사용자가 선호하는 옵션을 실험하여 고를 수 있습니다.\n\n이 옵션에 대해 잘 모른다면 끄기를 권장드립니다.", - "GraphicsAALabel": "안티 앨리어싱:", - "GraphicsScalingFilterLabel": "스케일링 필터:", - "GraphicsScalingFilterTooltip": "해상도 스케일에 사용될 스케일링 필터를 선택하세요.\n\nBilinear는 3D 게임에서 잘 작동하며 안전한 기본값입니다.\n\nNearest는 픽셀 아트 게임에 추천합니다.\n\nFSR 1.0은 그저 샤프닝 필터임으로, FXAA나 SMAA와 같이 사용하는 것은 권장하지 않습니다.\n\n이 옵션은 게임이 구동중일 때에도 아래 Apply 버튼을 눌러서 변경할 수 있습니다; 설정 창을 게임 창 옆에 두고 사용자가 선호하는 옵션을 실험하여 고를 수 있습니다.\n\n이 옵션에 대해 잘 모른다면 BILINEAR로 두세요.", - "GraphicsScalingFilterBilinear": "Bilinear", - "GraphicsScalingFilterNearest": "Nearest", + "UserProfilesRecoverEmptyList": "복구할 프로필 없음", + "GraphicsAATooltip": "게임 렌더에 앤티 앨리어싱을 적용합니다.\n\nFXAA는 이미지 대부분을 흐리게 처리하지만 SMAA는 들쭉날쭉한 가장자리를 찾아 부드럽게 처리합니다.\n\nFSR 스케일링 필터와 함께 사용하지 않는 것이 좋습니다.\n\n이 옵션은 아래의 \"적용\"을 클릭하여 게임을 실행하는 동안 변경할 수 있습니다. 설정 창을 옆으로 옮겨 원하는 게임의 모습을 찾을 때까지 실험해 볼 수 있습니다.\n\n모르면 없음으로 두세요.", + "GraphicsAALabel": "앤티 앨리어싱 :", + "GraphicsScalingFilterLabel": "크기 조정 필터 :", + "GraphicsScalingFilterTooltip": "해상도 스케일을 사용할 때 적용될 스케일링 필터를 선택합니다.\n\n쌍선형은 3D 게임에 적합하며 안전한 기본 옵션입니다.\n\nNearest는 픽셀 아트 게임에 권장됩니다.\n\nFSR 1.0은 단순히 선명도 필터일 뿐이며 FXAA 또는 SMAA와 함께 사용하는 것은 권장되지 않습니다.\n\nArea 스케일링은 출력 창보다 큰 해상도를 다운스케일링할 때 권장됩니다. 2배 이상 다운스케일링할 때 슈퍼샘플링된 앤티앨리어싱 효과를 얻는 데 사용할 수 있습니다.\n\n이 옵션은 아래의 \"적용\"을 클릭하여 게임을 실행하는 동안 변경할 수 있습니다. 설정 창을 옆으로 옮겨 원하는 게임 모양을 찾을 때까지 실험하면 됩니다.\n\n모르면 쌍선형을 그대로 두세요.", + "GraphicsScalingFilterBilinear": "쌍선형", + "GraphicsScalingFilterNearest": "근린", "GraphicsScalingFilterFsr": "FSR", - "GraphicsScalingFilterLevelLabel": "수준", - "GraphicsScalingFilterLevelTooltip": "FSR 1.0의 샤프닝 레벨을 설정하세요. 높을수록 더 또렷해집니다.", + "GraphicsScalingFilterArea": "영역", + "GraphicsScalingFilterLevelLabel": "레벨", + "GraphicsScalingFilterLevelTooltip": "FSR 1.0 선명도 레벨을 설정합니다. 높을수록 더 선명합니다.", "SmaaLow": "SMAA 낮음", "SmaaMedium": "SMAA 중간", "SmaaHigh": "SMAA 높음", "SmaaUltra": "SMAA 울트라", - "UserEditorTitle": "사용자 수정", - "UserEditorTitleCreate": "사용자 생성", + "UserEditorTitle": "사용자 편집", + "UserEditorTitleCreate": "사용자 만들기", "SettingsTabNetworkInterface": "네트워크 인터페이스:", - "NetworkInterfaceTooltip": "LAN/LDN 기능에 사용될 네트워크 인터페이스입니다.\n\nLAN 기능을 지원하는 게임에서 VPN이나 XLink Kai 등을 동시에 사용하면, 인터넷을 통해 동일 네트워크 연결인 것을 속일 수 있습니다.\n\n이 옵션에 대해 잘 모른다면 기본값으로 설정하세요.", - "NetworkInterfaceDefault": "기본", - "PackagingShaders": "셰이더 패키징 중", - "AboutChangelogButton": "GitHub에서 변경 로그 보기", - "AboutChangelogButtonTooltipMessage": "기본 브라우저에서 이 버전의 변경 로그를 열려면 클릭합니다.", - "SettingsTabNetworkMultiplayer": "멀티 플레이어", + "NetworkInterfaceTooltip": "LAN/LDN 기능에 사용되는 네트워크 인터페이스입니다.\n\nVPN이나 ​​XLink Kai와 LAN 지원 게임과 함께 사용하면 인터넷을 통한 동일 네트워크 연결을 스푸핑하는 데 사용할 수 있습니다.\n\n모르면 기본값으로 두세요.", + "NetworkInterfaceDefault": "기본값", + "PackagingShaders": "패키징 셰이더", + "AboutChangelogButton": "GitHub에서 변경 내역 보기", + "AboutChangelogButtonTooltipMessage": "기본 브라우저에서 이 버전의 변경 내역을 열람하려면 클릭하세요.", + "SettingsTabNetworkMultiplayer": "멀티플레이어", "MultiplayerMode": "모드 :", - "MultiplayerModeTooltip": "LDN 멀티플레이어 모드를 변경합니다.\n\nLdnMitm은 로컬 무선/로컬 플레이 기능을 수정하여 LAN 모드에 있는 것처럼 만들어 로컬이나 동일한 네트워크 상에 있는 다른 Ryujinx 인스턴스나 커펌된 닌텐도 스위치 콘솔(ldn_mitm 모듈 설치 필요)과 연결할 수 있습니다.\n\n멀티플레이어 모드는 모든 플레이어들이 동일한 게임 버전을 요구합니다. 예를 들어 슈퍼 스매시브라더스 얼티밋 v13.0.1 사용자는 v13.0.0 사용자와 연결할 수 없습니다.\n\n해당 옵션에 대해 잘 모른다면 비활성화해두세요.", + "MultiplayerModeTooltip": "LDN 멀티플레이어 모드를 변경합니다.\n\nLdnMitm은 게임의 로컬 무선/로컬 플레이 기능을 LAN처럼 작동하도록 수정하여 다른 Ryujinx 인스턴스나 ldn_mitm 모듈이 설치된 해킹된 Nintendo Switch 콘솔과 로컬, 동일 네트워크 연결이 가능합니다.\n\n멀티플레이어는 모든 플레이어가 동일한 게임 버전을 사용해야 합니다(예: Super Smash Bros. Ultimate v13.0.1은 v13.0.0에 연결할 수 없음).\n\n모르면 비활성화 상태로 두세요.", "MultiplayerModeDisabled": "비활성화됨", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "P2P 네트워크 호스팅 비활성화(대기 시간이 늘어날 수 있음)", + "MultiplayerDisableP2PTooltip": "P2P 네트워크 호스팅을 비활성화하면 피어가 직접 연결하지 않고 마스터 서버를 통해 프록시합니다.", + "LdnPassphrase": "네트워크 암호 문구 :", + "LdnPassphraseTooltip": "귀하는 귀하와 동일한 암호를 사용하는 호스팅 게임만 볼 수 있습니다.", + "LdnPassphraseInputTooltip": "Ryujinx-<8 hex chars> 형식으로 암호를 입력하세요. 귀하는 귀하와 동일한 암호를 사용하는 호스팅 게임만 볼 수 있습니다.", + "LdnPassphraseInputPublic": "(일반)", + "GenLdnPass": "무작위 생성", + "GenLdnPassTooltip": "다른 플레이어와 공유할 수 있는 새로운 암호 문구를 생성합니다.", + "ClearLdnPass": "지우기", + "ClearLdnPassTooltip": "현재 암호를 지우고 공용 네트워크로 돌아갑니다.", + "InvalidLdnPassphrase": "유효하지 않은 암호입니다! \"Ryujinx-<8 hex chars>\" 형식이어야 합니다." } diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json index a377979bd..1ed0988f9 100644 --- a/src/Ryujinx/Assets/Locales/pl_PL.json +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -1,6 +1,7 @@ { "Language": "Polski", "MenuBarFileOpenApplet": "Otwórz Aplet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Otwórz aplet Mii Editor w trybie indywidualnym", "SettingsTabInputDirectMouseAccess": "Bezpośredni dostęp do myszy", "SettingsTabSystemMemoryManagerMode": "Tryb menedżera pamięci:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "Użyj Hipernadzorcy", "MenuBarFile": "_Plik", "MenuBarFileOpenFromFile": "_Załaduj aplikację z pliku", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "Załaduj _rozpakowaną grę", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "Otwórz folder Ryujinx", "MenuBarFileOpenLogsFolder": "Otwórz folder plików dziennika zdarzeń", "MenuBarFileExit": "_Wyjdź", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "Zainstaluj oprogramowanie", "MenuBarFileToolsInstallFirmwareFromFile": "Zainstaluj oprogramowanie z XCI lub ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "Zainstaluj oprogramowanie z katalogu", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Zarządzaj rodzajami plików", "MenuBarToolsInstallFileTypes": "Typy plików instalacyjnych", "MenuBarToolsUninstallFileTypes": "Typy plików dezinstalacyjnych", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_View", "MenuBarViewWindow": "Window Size", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Otwiera katalog zawierający mody dla danej aplikacji", "GameListContextMenuOpenSdModsDirectory": "Otwórz katalog modów Atmosphere", "GameListContextMenuOpenSdModsDirectoryToolTip": "Otwiera alternatywny katalog Atmosphere na karcie SD, który zawiera mody danej aplikacji. Przydatne dla modów przygotowanych pod prawdziwy sprzęt.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} Załadowane gry", "StatusBarSystemVersion": "Wersja systemu: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Wykryto niski limit dla przypisań pamięci", "LinuxVmMaxMapCountDialogTextPrimary": "Czy chcesz zwiększyć wartość vm.max_map_count do {0}", "LinuxVmMaxMapCountDialogTextSecondary": "Niektóre gry mogą próbować przypisać sobie więcej pamięci niż obecnie, jest to dozwolone. Ryujinx ulegnie awarii, gdy limit zostanie przekroczony.", @@ -103,6 +114,8 @@ "SettingsTabGeneralHideCursorOnIdle": "Gdy bezczynny", "SettingsTabGeneralHideCursorAlways": "Zawsze", "SettingsTabGeneralGameDirectories": "Katalogi gier", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "Dodaj", "SettingsTabGeneralRemove": "Usuń", "SettingsTabSystem": "System", @@ -395,6 +408,8 @@ "InputDialogTitle": "Okno Dialogowe Wprowadzania", "InputDialogOk": "OK", "InputDialogCancel": "Anuluj", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "Wybierz nazwę profilu", "InputDialogAddNewProfileHeader": "Wprowadź nazwę profilu", "InputDialogAddNewProfileSubtext": "(Maksymalna długość: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "Ustaw kolor tła", "AvatarClose": "Zamknij", "ControllerSettingsLoadProfileToolTip": "Wczytaj profil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Dodaj profil", "ControllerSettingsRemoveProfileToolTip": "Usuń profil", "ControllerSettingsSaveProfileToolTip": "Zapisz profil", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "Przełącz na ulubione", "GameListContextMenuToggleFavoriteToolTip": "Przełącz status Ulubionej Gry", "SettingsTabGeneralTheme": "Motyw:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "Ciemny", "SettingsTabGeneralThemeLight": "Jasny", "ControllerSettingsConfigureGeneral": "Konfiguruj", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "Wystąpił błąd podczas próby znalezienia określonych zapisanych danych: {0}", "FolderDialogExtractTitle": "Wybierz folder, do którego chcesz rozpakować", "DialogNcaExtractionMessage": "Wypakowywanie sekcji {0} z {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Asystent wypakowania sekcji NCA", + "DialogNcaExtractionTitle": "Asystent wypakowania sekcji NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Niepowodzenie podczas wypakowywania. W wybranym pliku nie było głównego NCA.", "DialogNcaExtractionCheckLogErrorMessage": "Niepowodzenie podczas wypakowywania. Przeczytaj plik dziennika, aby uzyskać więcej informacji.", "DialogNcaExtractionSuccessMessage": "Wypakowywanie zakończone pomyślnie.", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "Wypakowywanie Aktualizacji...", "DialogUpdaterRenamingMessage": "Zmiana Nazwy Aktualizacji...", "DialogUpdaterAddingFilesMessage": "Dodawanie Nowej Aktualizacji...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Aktualizacja Zakończona!", "DialogUpdaterRestartMessage": "Czy chcesz teraz zrestartować Ryujinx?", "DialogUpdaterNoInternetMessage": "Nie masz połączenia z Internetem!", "DialogUpdaterNoInternetSubMessage": "Sprawdź, czy masz działające połączenie internetowe!", "DialogUpdaterDirtyBuildMessage": "Nie możesz zaktualizować Dirty wersji Ryujinx!", - "DialogUpdaterDirtyBuildSubMessage": "Pobierz Ryujinx ze strony https://https://github.com/GreemDev/Ryujinx/releases/, jeśli szukasz obsługiwanej wersji.", + "DialogUpdaterDirtyBuildSubMessage": "Pobierz Ryujinx ze strony https://ryujinx.app/download, jeśli szukasz obsługiwanej wersji.", "DialogRestartRequiredMessage": "Wymagane Ponowne Uruchomienie", "DialogThemeRestartMessage": "Motyw został zapisany. Aby zastosować motyw, konieczne jest ponowne uruchomienie.", "DialogThemeRestartSubMessage": "Czy chcesz uruchomić ponownie?", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "Pomyślnie odinstalowano typy plików!", "DialogUninstallFileTypesErrorMessage": "Nie udało się odinstalować typów plików.", "DialogOpenSettingsWindowLabel": "Otwórz Okno Ustawień", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "Aplet Kontrolera", "DialogMessageDialogErrorExceptionMessage": "Błąd wyświetlania okna Dialogowego Wiadomości: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Błąd wyświetlania Klawiatury Oprogramowania: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nCzy chcesz kontynuować?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalowanie firmware'u...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Wersja systemu {0} została pomyślnie zainstalowana.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "Nie będzie innych profili do otwarcia, jeśli wybrany profil zostanie usunięty", "DialogUserProfileDeletionConfirmMessage": "Czy chcesz usunąć wybrany profil", "DialogUserProfileUnsavedChangesTitle": "Uwaga - Niezapisane zmiany", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "Wprowadź katalog gier aby dodać go do listy", "AddGameDirTooltip": "Dodaj katalog gier do listy", "RemoveGameDirTooltip": "Usuń wybrany katalog gier", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "Użyj niestandardowego motywu Avalonia dla GUI, aby zmienić wygląd menu emulatora", "CustomThemePathTooltip": "Ścieżka do niestandardowego motywu GUI", "CustomThemeBrowseTooltip": "Wyszukaj niestandardowy motyw GUI", @@ -606,6 +635,8 @@ "DebugLogTooltip": "Wyświetla komunikaty dziennika debugowania w konsoli.\n\nUżywaj tego tylko na wyraźne polecenie członka załogi, ponieważ utrudni to odczytanie dzienników i pogorszy wydajność emulatora.", "LoadApplicationFileTooltip": "Otwórz eksplorator plików, aby wybrać plik kompatybilny z Switch do wczytania", "LoadApplicationFolderTooltip": "Otwórz eksplorator plików, aby wybrać zgodną z Switch, rozpakowaną aplikację do załadowania", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "Otwórz folder systemu plików Ryujinx", "OpenRyujinxLogsTooltip": "Otwiera folder, w którym zapisywane są logi", "ExitTooltip": "Wyjdź z Ryujinx", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "Otwórz Podręcznik Konfiguracji", "NoUpdate": "Brak Aktualizacji", "TitleUpdateVersionLabel": "Wersja {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Potwierdzenie", "FileDialogAllTypes": "Wszystkie typy", "Never": "Nigdy", "SwkbdMinCharacters": "Musi mieć co najmniej {0} znaków", "SwkbdMinRangeCharacters": "Musi mieć długość od {0}-{1} znaków", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Klawiatura Oprogramowania", "SoftwareKeyboardModeNumeric": "Może składać się jedynie z 0-9 lub '.'", "SoftwareKeyboardModeAlphabet": "Nie może zawierać znaków CJK", @@ -709,16 +751,53 @@ "SelectDlcDialogTitle": "Wybierz pliki DLC", "SelectUpdateDialogTitle": "Wybierz pliki aktualizacji", "SelectModDialogTitle": "Wybierz katalog modów", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "Menedżer Profili Użytkowników", "CheatWindowTitle": "Menedżer Kodów", "DlcWindowTitle": "Menedżer Zawartości do Pobrania", "ModWindowTitle": "Zarządzaj modami dla {0} ({1})", "UpdateWindowTitle": "Menedżer Aktualizacji Tytułu", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "Kody Dostępne dla {0} [{1}]", "BuildId": "Identyfikator wersji:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "DlcWindowHeading": "{0} Zawartości do Pobrania dostępna dla {1} ({2})", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} Mod(y/ów)", "UserProfilesEditProfile": "Edytuj Zaznaczone", + "Continue": "Continue", "Cancel": "Anuluj", "Save": "Zapisz", "Discard": "Odrzuć", @@ -767,6 +846,7 @@ "GraphicsScalingFilterBilinear": "Dwuliniowe", "GraphicsScalingFilterNearest": "Najbliższe", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "Poziom", "GraphicsScalingFilterLevelTooltip": "Ustaw poziom ostrzeżenia FSR 1.0. Wyższy jest ostrzejszy.", "SmaaLow": "SMAA Niskie", @@ -785,5 +865,17 @@ "MultiplayerMode": "Tryb:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", "MultiplayerModeDisabled": "Wyłączone", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json index 6aeb422ed..676d89d96 100644 --- a/src/Ryujinx/Assets/Locales/pt_BR.json +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -1,6 +1,7 @@ { "Language": "Português (BR)", "MenuBarFileOpenApplet": "Abrir Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Abrir editor Mii em modo avulso", "SettingsTabInputDirectMouseAccess": "Acesso direto ao mouse", "SettingsTabSystemMemoryManagerMode": "Modo de gerenciamento de memória:", @@ -30,9 +31,13 @@ "MenuBarToolsInstallFirmware": "_Instalar firmware", "MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware a partir de um arquivo ZIP/XCI", "MenuBarFileToolsInstallFirmwareFromDirectory": "Instalar firmware a partir de um diretório", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Gerenciar tipos de arquivo", "MenuBarToolsInstallFileTypes": "Instalar tipos de arquivo", "MenuBarToolsUninstallFileTypes": "Desinstalar tipos de arquivos", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_View", "MenuBarViewWindow": "Window Size", "MenuBarViewWindow720": "720p", @@ -84,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Abre a pasta que contém os mods da aplicação ", "GameListContextMenuOpenSdModsDirectory": "Abrir diretório de mods Atmosphere", "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} jogos carregados", "StatusBarSystemVersion": "Versão do firmware: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Limite baixo para mapeamentos de memória detectado", "LinuxVmMaxMapCountDialogTextPrimary": "Você gostaria de aumentar o valor de vm.max_map_count para {0}", "LinuxVmMaxMapCountDialogTextSecondary": "Alguns jogos podem tentar criar mais mapeamentos de memória do que o atualmente permitido. Ryujinx irá falhar assim que este limite for excedido.", @@ -400,6 +408,8 @@ "InputDialogTitle": "Diálogo de texto", "InputDialogOk": "OK", "InputDialogCancel": "Cancelar", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "Escolha o nome de perfil", "InputDialogAddNewProfileHeader": "Escreva o nome do perfil", "InputDialogAddNewProfileSubtext": "(Máximo de caracteres: {0})", @@ -407,6 +417,7 @@ "AvatarSetBackgroundColor": "Definir cor de fundo", "AvatarClose": "Fechar", "ControllerSettingsLoadProfileToolTip": "Carregar perfil", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Adicionar perfil", "ControllerSettingsRemoveProfileToolTip": "Remover perfil", "ControllerSettingsSaveProfileToolTip": "Salvar perfil", @@ -437,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "Ocorreu um erro ao tentar encontrar o diretório de salvamento: {0}", "FolderDialogExtractTitle": "Escolha o diretório onde os arquivos serão extraídos", "DialogNcaExtractionMessage": "Extraindo seção {0} de {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Extrator de seções NCA", + "DialogNcaExtractionTitle": "Extrator de seções NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Falha na extração. O NCA principal não foi encontrado no arquivo selecionado.", "DialogNcaExtractionCheckLogErrorMessage": "Falha na extração. Leia o arquivo de log para mais informações.", "DialogNcaExtractionSuccessMessage": "Extração concluída com êxito.", @@ -450,12 +461,13 @@ "DialogUpdaterExtractionMessage": "Extraindo atualização...", "DialogUpdaterRenamingMessage": "Renomeando atualização...", "DialogUpdaterAddingFilesMessage": "Adicionando nova atualização...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Atualização concluída!", "DialogUpdaterRestartMessage": "Deseja reiniciar o Ryujinx agora?", "DialogUpdaterNoInternetMessage": "Você não está conectado à Internet!", "DialogUpdaterNoInternetSubMessage": "Por favor, certifique-se de que você tem uma conexão funcional à Internet!", "DialogUpdaterDirtyBuildMessage": "Você não pode atualizar uma compilação Dirty do Ryujinx!", - "DialogUpdaterDirtyBuildSubMessage": "Por favor, baixe o Ryujinx em https://https://github.com/GreemDev/Ryujinx/releases/ se está procurando por uma versão suportada.", + "DialogUpdaterDirtyBuildSubMessage": "Por favor, baixe o Ryujinx em https://ryujinx.app/download se está procurando por uma versão suportada.", "DialogRestartRequiredMessage": "Reinicialização necessária", "DialogThemeRestartMessage": "O tema foi salvo. Uma reinicialização é necessária para aplicar o tema.", "DialogThemeRestartSubMessage": "Deseja reiniciar?", @@ -468,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "Tipos de arquivo desinstalados com sucesso!", "DialogUninstallFileTypesErrorMessage": "Falha ao desinstalar tipos de arquivo.", "DialogOpenSettingsWindowLabel": "Abrir janela de configurações", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "Applet de controle", "DialogMessageDialogErrorExceptionMessage": "Erro ao exibir diálogo de mensagem: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Erro ao exibir teclado virtual: {0}", @@ -496,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDeseja continuar?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalando firmware...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Versão do sistema {0} instalada com sucesso.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "Não haveria nenhum perfil selecionado se o perfil atual fosse deletado", "DialogUserProfileDeletionConfirmMessage": "Deseja deletar o perfil selecionado", "DialogUserProfileUnsavedChangesTitle": "Alerta - Alterações não salvas", @@ -617,7 +637,6 @@ "LoadApplicationFolderTooltip": "Abre o navegador de pastas para seleção de pasta extraída do Switch compatível a ser carregada", "OpenRyujinxFolderTooltip": "Abre o diretório do sistema de arquivos do Ryujinx", "LoadTitleUpdatesFromFolderTooltip": "Abra o explorador de arquivos para selecionar uma ou mais pastas e carregar atualizações de jogo em massa.", - "OpenRyujinxFolderTooltip": "Abrir diretório do sistema de arquivos do Ryujinx", "OpenRyujinxLogsTooltip": "Abre o diretório onde os logs são salvos", "ExitTooltip": "Sair do Ryujinx", "OpenSettingsTooltip": "Abrir janela de configurações", @@ -670,12 +689,21 @@ "TitleUpdateVersionLabel": "Versão {0}", "TitleBundledUpdateVersionLabel": "Empacotado: Versão {0}", "TitleBundledDlcLabel": "Empacotado:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - Informação", "RyujinxConfirm": "Ryujinx - Confirmação", "FileDialogAllTypes": "Todos os tipos", "Never": "Nunca", "SwkbdMinCharacters": "Deve ter pelo menos {0} caracteres", "SwkbdMinRangeCharacters": "Deve ter entre {0}-{1} caracteres", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Teclado por Software", "SoftwareKeyboardModeNumeric": "Deve ser somente 0-9 ou '.'", "SoftwareKeyboardModeAlphabet": "Apenas devem ser caracteres não CJK.", @@ -723,10 +751,38 @@ "SelectUpdateDialogTitle": "Selecionar arquivos de atualização", "SelectModDialogTitle": "Select mod directory", "UserProfileWindowTitle": "Gerenciador de perfis de usuário", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "CheatWindowTitle": "Gerenciador de Cheats", "DlcWindowTitle": "Gerenciador de DLC", "ModWindowTitle": "Gerenciar Mods para {0} ({1})", "UpdateWindowTitle": "Gerenciador de atualizações", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", "UpdateWindowUpdateAddedMessage": "{0} nova(s) atualização(ões) adicionada(s)", "UpdateWindowBundledContentNotice": "Atualizações incorporadas não podem ser removidas, apenas desativadas.", "CheatWindowHeading": "Cheats disponíveis para {0} [{1}]", @@ -740,6 +796,7 @@ "AutoloadUpdateRemovedMessage": "{0} atualização(ões) ausente(s) removida(s)", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Editar selecionado", + "Continue": "Continue", "Cancel": "Cancelar", "Save": "Salvar", "Discard": "Descartar", @@ -807,5 +864,17 @@ "MultiplayerMode": "Modo:", "MultiplayerModeTooltip": "Alterar o modo multiplayer LDN.\n\nLdnMitm modificará a funcionalidade de jogo sem fio/local nos jogos para funcionar como se fosse LAN, permitindo conexões locais, na mesma rede, com outras instâncias do Ryujinx e consoles Nintendo Switch hackeados que possuem o módulo ldn_mitm instalado.\n\nO multiplayer exige que todos os jogadores estejam na mesma versão do jogo (ex.: Super Smash Bros. Ultimate v13.0.1 não consegue se conectar à v13.0.0).\n\nDeixe DESATIVADO se estiver em dúvida.", "MultiplayerModeDisabled": "Desativado", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json index 8b9d39302..ea4dcc8c8 100644 --- a/src/Ryujinx/Assets/Locales/ru_RU.json +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -1,6 +1,7 @@ { "Language": "Русский (RU)", "MenuBarFileOpenApplet": "Открыть апплет", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Открывает апплет Mii Editor в автономном режиме", "SettingsTabInputDirectMouseAccess": "Прямой ввод мыши", "SettingsTabSystemMemoryManagerMode": "Режим менеджера памяти:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "Использовать Hypervisor", "MenuBarFile": "_Файл", "MenuBarFileOpenFromFile": "_Добавить приложение из файла", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "Добавить _распакованную игру", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "Открыть папку Ryujinx", "MenuBarFileOpenLogsFolder": "Открыть папку с логами", "MenuBarFileExit": "_Выход", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "Установка прошивки", "MenuBarFileToolsInstallFirmwareFromFile": "Установить прошивку из XCI или ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "Установить прошивку из папки", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Управление типами файлов", "MenuBarToolsInstallFileTypes": "Установить типы файлов", "MenuBarToolsUninstallFileTypes": "Удалить типы файлов", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_Вид", "MenuBarViewWindow": "Размер окна", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Открывает папку, содержащую моды для приложений и игр", "GameListContextMenuOpenSdModsDirectory": "Открыть папку с модами Atmosphere", "GameListContextMenuOpenSdModsDirectoryToolTip": "Открывает папку Atmosphere на альтернативной SD-карте, которая содержит моды для приложений и игр. Полезно для модов, сделанных для реальной консоли.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} игр загружено", "StatusBarSystemVersion": "Версия прошивки: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Обнаружен низкий лимит разметки памяти", "LinuxVmMaxMapCountDialogTextPrimary": "Хотите увеличить значение vm.max_map_count до {0}", "LinuxVmMaxMapCountDialogTextSecondary": "Некоторые игры могут создавать большую разметку памяти, чем разрешено на данный момент по умолчанию. Ryujinx вылетит при превышении этого лимита.", @@ -103,6 +114,8 @@ "SettingsTabGeneralHideCursorOnIdle": "В простое", "SettingsTabGeneralHideCursorAlways": "Всегда", "SettingsTabGeneralGameDirectories": "Папки с играми", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "Добавить", "SettingsTabGeneralRemove": "Удалить", "SettingsTabSystem": "Система", @@ -395,6 +408,8 @@ "InputDialogTitle": "Диалоговое окно ввода", "InputDialogOk": "ОК", "InputDialogCancel": "Отмена", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "Выберите никнейм", "InputDialogAddNewProfileHeader": "Пожалуйста, введите никнейм", "InputDialogAddNewProfileSubtext": "(Максимальная длина: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "Установить цвет фона", "AvatarClose": "Закрыть", "ControllerSettingsLoadProfileToolTip": "Загрузить профиль", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Добавить профиль", "ControllerSettingsRemoveProfileToolTip": "Удалить профиль", "ControllerSettingsSaveProfileToolTip": "Сохранить профиль", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "Добавить в избранное", "GameListContextMenuToggleFavoriteToolTip": "Добавляет игру в избранное и помечает звездочкой", "SettingsTabGeneralTheme": "Тема:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "Темная", "SettingsTabGeneralThemeLight": "Светлая", "ControllerSettingsConfigureGeneral": "Настройка", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "Произошла ошибка при поиске указанных данных сохранения: {0}", "FolderDialogExtractTitle": "Выберите папку для извлечения", "DialogNcaExtractionMessage": "Извлечение {0} раздела из {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Извлечение разделов NCA", + "DialogNcaExtractionTitle": "Извлечение разделов NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Ошибка извлечения. Основной NCA не присутствовал в выбранном файле.", "DialogNcaExtractionCheckLogErrorMessage": "Ошибка извлечения. Прочтите файл журнала для получения дополнительной информации.", "DialogNcaExtractionSuccessMessage": "Извлечение завершено успешно.", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "Извлечение обновления...", "DialogUpdaterRenamingMessage": "Переименование обновления...", "DialogUpdaterAddingFilesMessage": "Добавление нового обновления...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Обновление завершено", "DialogUpdaterRestartMessage": "Перезапустить Ryujinx?", "DialogUpdaterNoInternetMessage": "Вы не подключены к интернету", "DialogUpdaterNoInternetSubMessage": "Убедитесь, что у вас работает подключение к интернету", "DialogUpdaterDirtyBuildMessage": "Вы не можете обновлять Dirty Build", - "DialogUpdaterDirtyBuildSubMessage": "Загрузите Ryujinx по адресу https://https://github.com/GreemDev/Ryujinx/releases/ если вам нужна поддерживаемая версия.", + "DialogUpdaterDirtyBuildSubMessage": "Загрузите Ryujinx по адресу https://ryujinx.app/download если вам нужна поддерживаемая версия.", "DialogRestartRequiredMessage": "Требуется перезагрузка", "DialogThemeRestartMessage": "Тема сохранена. Для применения темы требуется перезапуск.", "DialogThemeRestartSubMessage": "Хотите перезапустить", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "Типы файлов успешно удалены", "DialogUninstallFileTypesErrorMessage": "Не удалось удалить типы файлов.", "DialogOpenSettingsWindowLabel": "Открывает окно параметров", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "Апплет контроллера", "DialogMessageDialogErrorExceptionMessage": "Ошибка отображения сообщения: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Ошибка отображения программной клавиатуры: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nПродолжить?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Установка прошивки...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Прошивка версии {0} успешно установлена.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "Если выбранный профиль будет удален, другие профили не будут открываться.", "DialogUserProfileDeletionConfirmMessage": "Удалить выбранный профиль?", "DialogUserProfileUnsavedChangesTitle": "Внимание - Несохраненные изменения", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "Введите путь к папке с играми для добавления ее в список выше", "AddGameDirTooltip": "Добавить папку с играми в список", "RemoveGameDirTooltip": "Удалить выбранную папку игры", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "Включить или отключить пользовательские темы", "CustomThemePathTooltip": "Путь к пользовательской теме для интерфейса", "CustomThemeBrowseTooltip": "Просмотр пользовательской темы интерфейса", @@ -606,6 +635,8 @@ "DebugLogTooltip": "Выводит журнал сообщений отладки в консоли.\n\nИспользуйте только в случае просьбы разработчика, так как включение этой функции затруднит чтение журналов и ухудшит работу эмулятора.", "LoadApplicationFileTooltip": "Открывает файловый менеджер для выбора файла, совместимого с Nintendo Switch.", "LoadApplicationFolderTooltip": "Открывает файловый менеджер для выбора распакованного приложения, совместимого с Nintendo Switch.", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "Открывает папку с файлами Ryujinx. ", "OpenRyujinxLogsTooltip": "Открывает папку в которую записываются логи", "ExitTooltip": "Выйти из Ryujinx", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "Открыть руководство по установке", "NoUpdate": "Без обновлений", "TitleUpdateVersionLabel": "Version {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - Информация", "RyujinxConfirm": "Ryujinx - Подтверждение", "FileDialogAllTypes": "Все типы", "Never": "Никогда", "SwkbdMinCharacters": "Должно быть не менее {0} символов.", "SwkbdMinRangeCharacters": "Должно быть {0}-{1} символов", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Программная клавиатура", "SoftwareKeyboardModeNumeric": "Должно быть в диапазоне 0-9 или '.'", "SoftwareKeyboardModeAlphabet": "Не должно быть CJK-символов", @@ -709,16 +751,53 @@ "SelectDlcDialogTitle": "Выберите файлы DLC", "SelectUpdateDialogTitle": "Выберите файлы обновлений", "SelectModDialogTitle": "Выбрать папку с модами", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "Менеджер учетных записей", "CheatWindowTitle": "Менеджер читов", "DlcWindowTitle": "Управление DLC для {0} ({1})", "ModWindowTitle": "Управление модами для {0} ({1})", "UpdateWindowTitle": "Менеджер обновлений игр", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "Доступные читы для {0} [{1}]", "BuildId": "ID версии:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "DlcWindowHeading": "{0} DLC", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "Моды для {0} ", "UserProfilesEditProfile": "Изменить выбранные", + "Continue": "Continue", "Cancel": "Отмена", "Save": "Сохранить", "Discard": "Отменить", @@ -767,6 +846,7 @@ "GraphicsScalingFilterBilinear": "Билинейная", "GraphicsScalingFilterNearest": "Ступенчатая", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "Уровень", "GraphicsScalingFilterLevelTooltip": "Выбор режима работы FSR 1.0. Выше - четче.", "SmaaLow": "SMAA Низкое", @@ -785,5 +865,17 @@ "MultiplayerMode": "Режим:", "MultiplayerModeTooltip": "Меняет многопользовательский режим LDN.\n\nLdnMitm модифицирует функциональность локальной беспроводной/игры на одном устройстве в играх, позволяя играть с другими пользователями Ryujinx или взломанными консолями Nintendo Switch с установленным модулем ldn_mitm, находящимися в одной локальной сети друг с другом.\n\nМногопользовательская игра требует наличия у всех игроков одной и той же версии игры (т.е. Super Smash Bros. Ultimate v13.0.1 не может подключиться к v13.0.0).\n\nРекомендуется оставить отключенным.", "MultiplayerModeDisabled": "Отключено", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json index 9e267dc9e..fa4c1d334 100644 --- a/src/Ryujinx/Assets/Locales/th_TH.json +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -1,6 +1,7 @@ { "Language": "ภาษาไทย", "MenuBarFileOpenApplet": "เปิด Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "เปิดโปรแกรม Mii Editor Applet", "SettingsTabInputDirectMouseAccess": "เข้าถึงเมาส์ได้โดยตรง", "SettingsTabSystemMemoryManagerMode": "โหมดจัดการหน่วยความจำ:", @@ -30,9 +31,13 @@ "MenuBarToolsInstallFirmware": "ติดตั้งเฟิร์มแวร์", "MenuBarFileToolsInstallFirmwareFromFile": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "ติดตั้งเฟิร์มแวร์จากไดเร็กทอรี", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "จัดการประเภทไฟล์", "MenuBarToolsInstallFileTypes": "ติดตั้งประเภทไฟล์", "MenuBarToolsUninstallFileTypes": "ถอนการติดตั้งประเภทไฟล์", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_มุมมอง", "MenuBarViewWindow": "ขนาดหน้าต่าง", "MenuBarViewWindow720": "720p", @@ -84,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "เปิดไดเร็กทอรี่ Mods ของแอปพลิเคชัน", "GameListContextMenuOpenSdModsDirectory": "เปิดไดเร็กทอรี่ Mods Atmosphere", "GameListContextMenuOpenSdModsDirectoryToolTip": "เปิดไดเร็กทอรี่ Atmosphere ของการ์ด SD สำรองซึ่งมี Mods ของแอปพลิเคชัน ซึ่งมีประโยชน์สำหรับ Mods ที่บรรจุมากับฮาร์ดแวร์จริง", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "เกมส์โหลดแล้ว {0}/{1}", "StatusBarSystemVersion": "เวอร์ชั่นของระบบ: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "การตั้งค่าหน่วยความถึงขีดจำกัดต่ำสุดแล้ว", "LinuxVmMaxMapCountDialogTextPrimary": "คุณต้องเพิ่มค่า vm.max_map_count ไปยัง {0}", "LinuxVmMaxMapCountDialogTextSecondary": "บางเกมอาจพยายามใช้งานหน่วยความจำมากกว่าที่ได้รับอนุญาตในปัจจุบัน Ryujinx จะปิดตัวลงเมื่อเกินขีดจำกัดนี้", @@ -107,6 +115,7 @@ "SettingsTabGeneralHideCursorAlways": "ตลอดเวลา", "SettingsTabGeneralGameDirectories": "ไดเรกทอรี่ของเกม", "SettingsTabGeneralAutoloadDirectories": "โหลดไดเรกทอรี DLC/ไฟล์อัปเดต อัตโนมัติ", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "เพิ่ม", "SettingsTabGeneralRemove": "เอาออก", "SettingsTabSystem": "ระบบ", @@ -399,6 +408,8 @@ "InputDialogTitle": "กล่องโต้ตอบการป้อนข้อมูล", "InputDialogOk": "ตกลง", "InputDialogCancel": "ยกเลิก", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "เลือก ชื่อโปรไฟล์", "InputDialogAddNewProfileHeader": "กรุณาใส่ชื่อโปรไฟล์", "InputDialogAddNewProfileSubtext": "(ความยาวสูงสุด: {0})", @@ -406,6 +417,7 @@ "AvatarSetBackgroundColor": "ตั้งค่าสีพื้นหลัง", "AvatarClose": "ปิด", "ControllerSettingsLoadProfileToolTip": "โหลด โปรไฟล์", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "เพิ่ม โปรไฟล์", "ControllerSettingsRemoveProfileToolTip": "ลบ โปรไฟล์", "ControllerSettingsSaveProfileToolTip": "บันทึก โปรไฟล์", @@ -436,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "มีข้อผิดพลาดในการค้นหาข้อมูลบันทึกที่ระบุไว้: {0}", "FolderDialogExtractTitle": "เลือกโฟลเดอร์ที่จะแตกไฟล์เข้าไป", "DialogNcaExtractionMessage": "กำลังแตกไฟล์ {0} จากส่วน {1}...", - "DialogNcaExtractionTitle": "Ryujinx - เครื่องมือแตกไฟล์ของ NCA", + "DialogNcaExtractionTitle": "เครื่องมือแตกไฟล์ของ NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "เกิดความล้มเหลวในการแตกไฟล์เนื่องจากไม่พบ NCA หลักในไฟล์ที่เลือก", "DialogNcaExtractionCheckLogErrorMessage": "เกิดความล้มเหลวในการแตกไฟล์ โปรดอ่านไฟล์บันทึกประวัติเพื่อดูข้อมูลเพิ่มเติม", "DialogNcaExtractionSuccessMessage": "การแตกไฟล์เสร็จสมบูรณ์แล้ว", @@ -449,12 +461,13 @@ "DialogUpdaterExtractionMessage": "กำลังแตกไฟล์อัปเดต...", "DialogUpdaterRenamingMessage": "กำลังลบไฟล์เก่า...", "DialogUpdaterAddingFilesMessage": "กำลังเพิ่มไฟล์อัปเดตใหม่...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "อัปเดตเสร็จสมบูรณ์แล้ว!", "DialogUpdaterRestartMessage": "คุณต้องการรีสตาร์ท Ryujinx ตอนนี้หรือไม่?", "DialogUpdaterNoInternetMessage": "คุณไม่ได้เชื่อมต่อกับอินเทอร์เน็ต!", "DialogUpdaterNoInternetSubMessage": "โปรดตรวจสอบว่าคุณมีการเชื่อมต่ออินเทอร์เน็ตว่ามีการใช้งานได้หรือไม่!", "DialogUpdaterDirtyBuildMessage": "คุณไม่สามารถอัปเดต Dirty build ของ Ryujinx ได้!", - "DialogUpdaterDirtyBuildSubMessage": "โปรดดาวน์โหลด Ryujinx ได้ที่ https://https://github.com/GreemDev/Ryujinx/releases/ หากคุณกำลังมองหาเวอร์ชั่นที่รองรับ", + "DialogUpdaterDirtyBuildSubMessage": "โปรดดาวน์โหลด Ryujinx ได้ที่ https://ryujinx.app/download หากคุณกำลังมองหาเวอร์ชั่นที่รองรับ", "DialogRestartRequiredMessage": "จำเป็นต้องรีสตาร์ทเพื่อให้การอัพเดตสามารถให้งานได้", "DialogThemeRestartMessage": "บันทึกธีมแล้ว จำเป็นต้องรีสตาร์ทเพื่อใช้ธีม", "DialogThemeRestartSubMessage": "คุณต้องการรีสตาร์ทหรือไม่?", @@ -467,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "ถอนการติดตั้งตามประเภทของไฟล์สำเร็จแล้ว!", "DialogUninstallFileTypesErrorMessage": "ไม่สามารถถอนการติดตั้งตามประเภทของไฟล์ได้", "DialogOpenSettingsWindowLabel": "เปิดหน้าต่างการตั้งค่า", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "คอนโทรลเลอร์ Applet", "DialogMessageDialogErrorExceptionMessage": "เกิดข้อผิดพลาดในการแสดงกล่องโต้ตอบข้อความ: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "เกิดข้อผิดพลาดในการแสดงซอฟต์แวร์แป้นพิมพ์: {0}", @@ -495,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nคุณต้องการดำเนินการต่อหรือไม่?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "กำลังติดตั้งเฟิร์มแวร์...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "ระบบเวอร์ชั่น {0} ติดตั้งเรียบร้อยแล้ว", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "จะไม่มีโปรไฟล์อื่นให้เปิดหากโปรไฟล์ที่เลือกถูกลบ", "DialogUserProfileDeletionConfirmMessage": "คุณต้องการลบโปรไฟล์ที่เลือกหรือไม่?", "DialogUserProfileUnsavedChangesTitle": "คำเตือน - มีการเปลี่ยนแปลงที่ไม่ได้บันทึก", @@ -669,12 +690,21 @@ "TitleUpdateVersionLabel": "เวอร์ชั่น {0}", "TitleBundledUpdateVersionLabel": "Bundled: เวอร์ชั่น {0}", "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx – ข้อมูล", "RyujinxConfirm": "Ryujinx - ยืนยัน", "FileDialogAllTypes": "ทุกประเภท", "Never": "ไม่ต้อง", "SwkbdMinCharacters": "ต้องมีความยาวของตัวอักษรอย่างน้อย {0} ตัว", "SwkbdMinRangeCharacters": "ต้องมีความยาวของตัวอักษร {0}-{1} ตัว", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "ซอฟต์แวร์คีย์บอร์ด", "SoftwareKeyboardModeNumeric": "ต้องเป็น 0-9 หรือ '.' เท่านั้น", "SoftwareKeyboardModeAlphabet": "ต้องเป็นตัวอักษรที่ไม่ใช่ประเภท CJK เท่านั้น", @@ -721,11 +751,39 @@ "SelectDlcDialogTitle": "เลือกไฟล์ DLC", "SelectUpdateDialogTitle": "เลือกไฟล์อัพเดต", "SelectModDialogTitle": "เลือกไดเรกทอรี Mods", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "จัดการโปรไฟล์ผู้ใช้", "CheatWindowTitle": "จัดการสูตรโกง", "DlcWindowTitle": "จัดการ DLC ที่ดาวน์โหลดได้สำหรับ {0} ({1})", "ModWindowTitle": "จัดการม็อดที่ดาวน์โหลดได้สำหรับ {0} ({1})", "UpdateWindowTitle": "จัดการอัปเดตหัวข้อ", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", "UpdateWindowUpdateAddedMessage": "{0} อัพเดตที่เพิ่มมาใหม่", "UpdateWindowBundledContentNotice": "แพ็คที่อัพเดตมาไม่สามารถลบทิ้งได้ สามารถปิดใช้งานได้เท่านั้น", "CheatWindowHeading": "สูตรโกงมีให้สำหรับ {0} [{1}]", @@ -734,10 +792,12 @@ "DlcWindowHeading": "{0} DLC ที่สามารถดาวน์โหลดได้", "DlcWindowDlcAddedMessage": "{0} DLC ใหม่ที่เพิ่มเข้ามา", "AutoloadDlcAddedMessage": "{0} ใหม่ที่เพิ่มเข้ามา", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", "AutoloadUpdateAddedMessage": "{0} อัพเดตใหม่ที่เพิ่มเข้ามา", - "AutoloadDlcAndUpdateAddedMessage": "{0} DLC ใหม่ที่เพิ่มเข้ามาและ {1} อัพเดตใหม่ที่เพิ่มเข้ามา", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} ม็อด", "UserProfilesEditProfile": "แก้ไขที่เลือกแล้ว", + "Continue": "Continue", "Cancel": "ยกเลิก", "Save": "บันทึก", "Discard": "ละทิ้ง", @@ -805,5 +865,17 @@ "MultiplayerMode": "โหมด:", "MultiplayerModeTooltip": "เปลี่ยนโหมดผู้เล่นหลายคนของ LDN\n\nLdnMitm จะปรับเปลี่ยนฟังก์ชันการเล่นแบบไร้สาย/ภายใน จะให้เกมทำงานเหมือนกับว่าเป็น LAN ช่วยให้สามารถเชื่อมต่อภายในเครือข่ายเดียวกันกับอินสแตนซ์ Ryujinx อื่น ๆ และคอนโซล Nintendo Switch ที่ถูกแฮ็กซึ่งมีโมดูล ldn_mitm ติดตั้งอยู่\n\nผู้เล่นหลายคนต้องการให้ผู้เล่นทุกคนอยู่ในเกมเวอร์ชันเดียวกัน (เช่น Super Smash Bros. Ultimate v13.0.1 ไม่สามารถเชื่อมต่อกับ v13.0.0)\n\nปล่อยให้ปิดการใช้งานหากไม่แน่ใจ", "MultiplayerModeDisabled": "ปิดใช้งาน", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json index 1360a122e..475086e44 100644 --- a/src/Ryujinx/Assets/Locales/tr_TR.json +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -1,6 +1,7 @@ { "Language": "Türkçe", "MenuBarFileOpenApplet": "Applet'i Aç", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Mii Editör Applet'ini Bağımsız Mod'da Aç", "SettingsTabInputDirectMouseAccess": "Doğrudan Mouse Erişimi", "SettingsTabSystemMemoryManagerMode": "Hafıza Yönetim Modu:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "Hypervisor Kullan", "MenuBarFile": "_Dosya", "MenuBarFileOpenFromFile": "_Dosyadan Uygulama Aç", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "_Sıkıştırılmamış Oyun Aç", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "Ryujinx Klasörünü aç", "MenuBarFileOpenLogsFolder": "Logs Klasörünü aç", "MenuBarFileExit": "_Çıkış", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "Yazılım Yükle", "MenuBarFileToolsInstallFirmwareFromFile": "XCI veya ZIP'ten Yazılım Yükle", "MenuBarFileToolsInstallFirmwareFromDirectory": "Bir Dizin Üzerinden Yazılım Yükle", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Dosya uzantılarını yönet", "MenuBarToolsInstallFileTypes": "Dosya uzantılarını yükle", "MenuBarToolsUninstallFileTypes": "Dosya uzantılarını kaldır", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_Görüntüle", "MenuBarViewWindow": "Pencere Boyutu", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods", "GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory", "GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} Oyun Yüklendi", "StatusBarSystemVersion": "Sistem Sürümü: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Bellek Haritaları İçin Düşük Limit Tespit Edildi ", "LinuxVmMaxMapCountDialogTextPrimary": "vm.max_map_count değerini {0} sayısına yükseltmek ister misiniz", "LinuxVmMaxMapCountDialogTextSecondary": "Bazı oyunlar şu an izin verilen bellek haritası limitinden daha fazlasını yaratmaya çalışabilir. Ryujinx bu limitin geçildiği takdirde kendini kapatıcaktır.", @@ -103,6 +114,8 @@ "SettingsTabGeneralHideCursorOnIdle": "Hareketsiz Durumda", "SettingsTabGeneralHideCursorAlways": "Her Zaman", "SettingsTabGeneralGameDirectories": "Oyun Dizinleri", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "Ekle", "SettingsTabGeneralRemove": "Kaldır", "SettingsTabSystem": "Sistem", @@ -395,6 +408,8 @@ "InputDialogTitle": "Giriş Yöntemi Diyaloğu", "InputDialogOk": "Tamam", "InputDialogCancel": "İptal", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "Profil İsmini Seç", "InputDialogAddNewProfileHeader": "Lütfen Bir Profil İsmi Girin", "InputDialogAddNewProfileSubtext": "(Maksimum Uzunluk: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "Arka Plan Rengi Ayarla", "AvatarClose": "Kapat", "ControllerSettingsLoadProfileToolTip": "Profil Yükle", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Profil Ekle", "ControllerSettingsRemoveProfileToolTip": "Profili Kaldır", "ControllerSettingsSaveProfileToolTip": "Profili Kaydet", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "Favori Ayarla", "GameListContextMenuToggleFavoriteToolTip": "Oyunu Favorilere Ekle/Çıkar", "SettingsTabGeneralTheme": "Tema:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "Karanlık", "SettingsTabGeneralThemeLight": "Aydınlık", "ControllerSettingsConfigureGeneral": "Ayarla", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "Belirtilen kayıt verisi bulunmaya çalışırken hata: {0}", "FolderDialogExtractTitle": "İçine ayıklanacak klasörü seç", "DialogNcaExtractionMessage": "{1} den {0} kısmı ayıklanıyor...", - "DialogNcaExtractionTitle": "Ryujinx - NCA Kısmı Ayıklayıcısı", + "DialogNcaExtractionTitle": "NCA Kısmı Ayıklayıcısı", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Ayıklama hatası. Ana NCA seçilen dosyada bulunamadı.", "DialogNcaExtractionCheckLogErrorMessage": "Ayıklama hatası. Ek bilgi için kayıt dosyasını okuyun.", "DialogNcaExtractionSuccessMessage": "Ayıklama başarıyla tamamlandı.", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "Güncelleme Ayıklanıyor...", "DialogUpdaterRenamingMessage": "Güncelleme Yeniden Adlandırılıyor...", "DialogUpdaterAddingFilesMessage": "Yeni Güncelleme Ekleniyor...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Güncelleme Tamamlandı!", "DialogUpdaterRestartMessage": "Ryujinx'i şimdi yeniden başlatmak istiyor musunuz?", "DialogUpdaterNoInternetMessage": "İnternete bağlı değilsiniz!", "DialogUpdaterNoInternetSubMessage": "Lütfen aktif bir internet bağlantınız olduğunu kontrol edin!", "DialogUpdaterDirtyBuildMessage": "Ryujinx'in Dirty build'lerini güncelleyemezsiniz!", - "DialogUpdaterDirtyBuildSubMessage": "Desteklenen bir sürüm için lütfen Ryujinx'i https://https://github.com/GreemDev/Ryujinx/releases/ sitesinden indirin.", + "DialogUpdaterDirtyBuildSubMessage": "Desteklenen bir sürüm için lütfen Ryujinx'i https://ryujinx.app/download sitesinden indirin.", "DialogRestartRequiredMessage": "Yeniden Başlatma Gerekli", "DialogThemeRestartMessage": "Tema kaydedildi. Temayı uygulamak için yeniden başlatma gerekiyor.", "DialogThemeRestartSubMessage": "Yeniden başlatmak ister misiniz", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "Dosya uzantıları başarıyla kaldırıldı!", "DialogUninstallFileTypesErrorMessage": "Dosya uzantıları kaldırma işlemi başarısız oldu.", "DialogOpenSettingsWindowLabel": "Seçenekler Penceresini Aç", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "Kumanda Applet'i", "DialogMessageDialogErrorExceptionMessage": "Mesaj diyaloğu gösterilirken hata: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Mesaj diyaloğu gösterilirken hata: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDevam etmek istiyor musunuz?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Firmware yükleniyor...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Sistem sürümü {0} başarıyla yüklendi.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "Seçilen profil silinirse kullanılabilen başka profil kalmayacak", "DialogUserProfileDeletionConfirmMessage": "Seçilen profili silmek istiyor musunuz", "DialogUserProfileUnsavedChangesTitle": "Uyarı - Kaydedilmemiş Değişiklikler", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "Listeye eklemek için oyun dizini seçin", "AddGameDirTooltip": "Listeye oyun dizini ekle", "RemoveGameDirTooltip": "Seçili oyun dizinini kaldır", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "Emülatör pencerelerinin görünümünü değiştirmek için özel bir Avalonia teması kullan", "CustomThemePathTooltip": "Özel arayüz temasının yolu", "CustomThemeBrowseTooltip": "Özel arayüz teması için göz at", @@ -606,6 +635,8 @@ "DebugLogTooltip": "Debug log mesajlarını konsola yazdırır.\n\nBu seçeneği yalnızca geliştirici üyemiz belirtirse aktifleştirin, çünkü bu seçenek log dosyasını okumayı zorlaştırır ve emülatörün performansını düşürür.", "LoadApplicationFileTooltip": "Switch ile uyumlu bir dosya yüklemek için dosya tarayıcısını açar", "LoadApplicationFolderTooltip": "Switch ile uyumlu ayrıştırılmamış bir uygulama yüklemek için dosya tarayıcısını açar", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "Ryujinx dosya sistem klasörünü açar", "OpenRyujinxLogsTooltip": "Log dosyalarının bulunduğu klasörü açar", "ExitTooltip": "Ryujinx'ten çıkış yapmayı sağlar", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "Kurulum Kılavuzunu Aç", "NoUpdate": "Güncelleme Yok", "TitleUpdateVersionLabel": "Sürüm {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - Bilgi", "RyujinxConfirm": "Ryujinx - Doğrulama", "FileDialogAllTypes": "Tüm türler", "Never": "Hiçbir Zaman", "SwkbdMinCharacters": "En az {0} karakter uzunluğunda olmalı", "SwkbdMinRangeCharacters": "{0}-{1} karakter uzunluğunda olmalı", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Yazılım Klavyesi", "SoftwareKeyboardModeNumeric": "Sadece 0-9 veya '.' olabilir", "SoftwareKeyboardModeAlphabet": "Sadece CJK-characters olmayan karakterler olabilir", @@ -709,16 +751,53 @@ "SelectDlcDialogTitle": "DLC dosyalarını seç", "SelectUpdateDialogTitle": "Güncelleme dosyalarını seç", "SelectModDialogTitle": "Mod Dizinini Seç", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "Kullanıcı Profillerini Yönet", "CheatWindowTitle": "Oyun Hilelerini Yönet", "DlcWindowTitle": "Oyun DLC'lerini Yönet", "ModWindowTitle": "Manage Mods for {0} ({1})", "UpdateWindowTitle": "Oyun Güncellemelerini Yönet", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "{0} için Hile mevcut [{1}]", "BuildId": "BuildId:", - "DlcWindowHeading": "{0} için DLC mevcut [{1}]", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", + "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} Mod(lar)", "UserProfilesEditProfile": "Seçiliyi Düzenle", + "Continue": "Continue", "Cancel": "İptal", "Save": "Kaydet", "Discard": "Iskarta", @@ -767,6 +846,7 @@ "GraphicsScalingFilterBilinear": "Bilinear", "GraphicsScalingFilterNearest": "Nearest", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "Seviye", "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.", "SmaaLow": "Düşük SMAA", @@ -785,5 +865,17 @@ "MultiplayerMode": "Mod:", "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.", "MultiplayerModeDisabled": "Devre Dışı", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json index 2fe5758b5..68679a9b2 100644 --- a/src/Ryujinx/Assets/Locales/uk_UA.json +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -1,6 +1,7 @@ { "Language": "Українська", "MenuBarFileOpenApplet": "Відкрити аплет", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Відкрити аплет Mii Editor в автономному режимі", "SettingsTabInputDirectMouseAccess": "Прямий доступ мишею", "SettingsTabSystemMemoryManagerMode": "Режим диспетчера пам’яті:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "Використовувати гіпервізор", "MenuBarFile": "_Файл", "MenuBarFileOpenFromFile": "_Завантажити програму з файлу", + "MenuBarFileOpenFromFileError": "No applications found in selected file.", "MenuBarFileOpenUnpacked": "Завантажити _розпаковану гру", + "MenuBarFileLoadDlcFromFolder": "Load DLC From Folder", + "MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder", "MenuBarFileOpenEmuFolder": "Відкрити теку Ryujinx", "MenuBarFileOpenLogsFolder": "Відкрити теку журналів змін", "MenuBarFileExit": "_Вихід", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "Установити прошивку", "MenuBarFileToolsInstallFirmwareFromFile": "Установити прошивку з XCI або ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "Установити прошивку з теки", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "Керувати типами файлів", "MenuBarToolsInstallFileTypes": "Установити типи файлів", "MenuBarToolsUninstallFileTypes": "Видалити типи файлів", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "_View", "MenuBarViewWindow": "Window Size", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Відкриває каталог, який містить модифікації Додатків", "GameListContextMenuOpenSdModsDirectory": "Відкрити каталог модифікацій Atmosphere", "GameListContextMenuOpenSdModsDirectoryToolTip": "Відкриває альтернативний каталог SD-карти Atmosphere, що містить модифікації Додатків. Корисно для модифікацій, зроблених для реального обладнання.", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} ігор завантажено", "StatusBarSystemVersion": "Версія системи: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "Виявлено низьку межу для відображення памʼяті", "LinuxVmMaxMapCountDialogTextPrimary": "Бажаєте збільшити значення vm.max_map_count на {0}", "LinuxVmMaxMapCountDialogTextSecondary": "Деякі ігри можуть спробувати створити більше відображень памʼяті, ніж дозволено наразі. Ryujinx завершить роботу, щойно цей ліміт буде перевищено.", @@ -103,6 +114,8 @@ "SettingsTabGeneralHideCursorOnIdle": "Сховати у режимі очікування", "SettingsTabGeneralHideCursorAlways": "Завжди", "SettingsTabGeneralGameDirectories": "Тека ігор", + "SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories", + "SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically", "SettingsTabGeneralAdd": "Додати", "SettingsTabGeneralRemove": "Видалити", "SettingsTabSystem": "Система", @@ -395,6 +408,8 @@ "InputDialogTitle": "Діалог введення", "InputDialogOk": "Гаразд", "InputDialogCancel": "Скасувати", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "Виберіть ім'я профілю", "InputDialogAddNewProfileHeader": "Будь ласка, введіть ім'я профілю", "InputDialogAddNewProfileSubtext": "(Макс. довжина: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "Встановити колір фону", "AvatarClose": "Закрити", "ControllerSettingsLoadProfileToolTip": "Завантажити профіль", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "Додати профіль", "ControllerSettingsRemoveProfileToolTip": "Видалити профіль", "ControllerSettingsSaveProfileToolTip": "Зберегти профіль", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "Перемкнути вибране", "GameListContextMenuToggleFavoriteToolTip": "Перемкнути улюблений статус гри", "SettingsTabGeneralTheme": "Тема:", + "SettingsTabGeneralThemeAuto": "Auto", "SettingsTabGeneralThemeDark": "Темна", "SettingsTabGeneralThemeLight": "Світла", "ControllerSettingsConfigureGeneral": "Налаштування", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "Під час пошуку вказаних даних збереження сталася помилка: {0}", "FolderDialogExtractTitle": "Виберіть папку для видобування", "DialogNcaExtractionMessage": "Видобування розділу {0} з {1}...", - "DialogNcaExtractionTitle": "Ryujinx - Екстрактор розділів NCA", + "DialogNcaExtractionTitle": "Екстрактор розділів NCA", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "Помилка видобування. Основний NCA не був присутній у вибраному файлі.", "DialogNcaExtractionCheckLogErrorMessage": "Помилка видобування. Прочитайте файл журналу для отримання додаткової інформації.", "DialogNcaExtractionSuccessMessage": "Видобування успішно завершено.", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "Видобування оновлення...", "DialogUpdaterRenamingMessage": "Перейменування оновлення...", "DialogUpdaterAddingFilesMessage": "Додавання нового оновлення...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "Оновлення завершено!", "DialogUpdaterRestartMessage": "Перезапустити Ryujinx зараз?", "DialogUpdaterNoInternetMessage": "Ви не підключені до Інтернету!", "DialogUpdaterNoInternetSubMessage": "Будь ласка, переконайтеся, що у вас є робоче підключення до Інтернету!", "DialogUpdaterDirtyBuildMessage": "Ви не можете оновити брудну збірку Ryujinx!", - "DialogUpdaterDirtyBuildSubMessage": "Будь ласка, завантажте Ryujinx на https://https://github.com/GreemDev/Ryujinx/releases/, якщо ви шукаєте підтримувану версію.", + "DialogUpdaterDirtyBuildSubMessage": "Будь ласка, завантажте Ryujinx на https://ryujinx.app/download, якщо ви шукаєте підтримувану версію.", "DialogRestartRequiredMessage": "Потрібен перезапуск", "DialogThemeRestartMessage": "Тему збережено. Щоб застосувати тему, потрібен перезапуск.", "DialogThemeRestartSubMessage": "Ви хочете перезапустити", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "Успішно видалено типи файлів!", "DialogUninstallFileTypesErrorMessage": "Не вдалося видалити типи файлів.", "DialogOpenSettingsWindowLabel": "Відкрити вікно налаштувань", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "Аплет контролера", "DialogMessageDialogErrorExceptionMessage": "Помилка показу діалогового вікна повідомлення: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Помилка показу програмної клавіатури: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nВи хочете продовжити?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Встановлення прошивки...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Версію системи {0} успішно встановлено.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "Якщо вибраний профіль буде видалено, інші профілі не відкриватимуться", "DialogUserProfileDeletionConfirmMessage": "Ви хочете видалити вибраний профіль", "DialogUserProfileUnsavedChangesTitle": "Увага — Незбережені зміни", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "Введіть каталог ігор, щоб додати до списку", "AddGameDirTooltip": "Додати каталог гри до списку", "RemoveGameDirTooltip": "Видалити вибраний каталог гри", + "AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list", + "AddAutoloadDirTooltip": "Add an autoload directory to the list", + "RemoveAutoloadDirTooltip": "Remove selected autoload directory", "CustomThemeCheckTooltip": "Використовуйте користувацьку тему Avalonia для графічного інтерфейсу, щоб змінити вигляд меню емулятора", "CustomThemePathTooltip": "Шлях до користувацької теми графічного інтерфейсу", "CustomThemeBrowseTooltip": "Огляд користувацької теми графічного інтерфейсу", @@ -606,6 +635,8 @@ "DebugLogTooltip": "Друкує повідомлення журналу налагодження на консолі.\n\nВикористовуйте це лише за спеціальною вказівкою співробітника, оскільки це ускладнить читання журналів і погіршить роботу емулятора.", "LoadApplicationFileTooltip": "Відкриває файловий провідник, щоб вибрати для завантаження сумісний файл Switch", "LoadApplicationFolderTooltip": "Відкриває файловий провідник, щоб вибрати сумісну з комутатором розпаковану програму для завантаження", + "LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from", + "LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from", "OpenRyujinxFolderTooltip": "Відкриває папку файлової системи Ryujinx", "OpenRyujinxLogsTooltip": "Відкриває папку, куди записуються журнали", "ExitTooltip": "Виходить з Ryujinx", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "Відкрити посібник із налаштування", "NoUpdate": "Немає оновлень", "TitleUpdateVersionLabel": "Версія {0} - {1}", + "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", + "TitleBundledDlcLabel": "Bundled:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujin x - Інформація", "RyujinxConfirm": "Ryujinx - Підтвердження", "FileDialogAllTypes": "Всі типи", "Never": "Ніколи", "SwkbdMinCharacters": "Мінімальна кількість символів: {0}", "SwkbdMinRangeCharacters": "Має бути {0}-{1} символів", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Програмна клавіатура", "SoftwareKeyboardModeNumeric": "Повинно бути лише 0-9 або “.”", "SoftwareKeyboardModeAlphabet": "Повинно бути лише не CJK-символи", @@ -709,16 +751,53 @@ "SelectDlcDialogTitle": "Виберіть файли DLC", "SelectUpdateDialogTitle": "Виберіть файли оновлення", "SelectModDialogTitle": "Виберіть теку з модами", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "Менеджер профілів користувачів", "CheatWindowTitle": "Менеджер читів", "DlcWindowTitle": "Менеджер вмісту для завантаження", "ModWindowTitle": "Керувати модами для {0} ({1})", "UpdateWindowTitle": "Менеджер оновлення назв", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "{0} new update(s) added", + "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "Коди доступні для {0} [{1}]", "BuildId": "ID збірки:", + "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "DlcWindowHeading": "Вміст для завантаження, доступний для {1} ({2}): {0}", + "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", + "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", + "AutoloadUpdateAddedMessage": "{0} new update(s) added", + "AutoloadUpdateRemovedMessage": "{0} missing update(s) removed", "ModWindowHeading": "{0} мод(ів)", "UserProfilesEditProfile": "Редагувати вибране", + "Continue": "Continue", "Cancel": "Скасувати", "Save": "Зберегти", "Discard": "Скасувати", @@ -767,6 +846,7 @@ "GraphicsScalingFilterBilinear": "Білінійний", "GraphicsScalingFilterNearest": "Найближчий", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "Рівень", "GraphicsScalingFilterLevelTooltip": "Встановити рівень різкості в FSR 1.0. Чим вище - тим різкіше.", "SmaaLow": "SMAA Низький", @@ -785,5 +865,17 @@ "MultiplayerMode": "Режим:", "MultiplayerModeTooltip": "Змінити LDN мультиплеєру.\n\nLdnMitm змінить функціонал бездротової/локальної гри в іграх, щоб вони працювали так, ніби це LAN, що дозволяє локальні підключення в тій самій мережі з іншими екземплярами Ryujinx та хакнутими консолями Nintendo Switch, які мають встановлений модуль ldn_mitm.\n\nМультиплеєр вимагає, щоб усі гравці були на одній і тій же версії гри (наприклад Super Smash Bros. Ultimate v13.0.1 не зможе під'єднатися до v13.0.0).\n\nЗалиште на \"Вимкнено\", якщо не впевнені, ", "MultiplayerModeDisabled": "Вимкнено", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json index e0fd15922..741b5b370 100644 --- a/src/Ryujinx/Assets/Locales/zh_CN.json +++ b/src/Ryujinx/Assets/Locales/zh_CN.json @@ -1,12 +1,13 @@ { "Language": "简体中文", "MenuBarFileOpenApplet": "打开小程序", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "打开独立的 Mii 小程序", "SettingsTabInputDirectMouseAccess": "直通鼠标操作", "SettingsTabSystemMemoryManagerMode": "内存管理模式:", "SettingsTabSystemMemoryManagerModeSoftware": "软件管理", "SettingsTabSystemMemoryManagerModeHost": "本机映射 (较快)", - "SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机映射 (最快,但不安全)", + "SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机映射 (最快,不安全)", "SettingsTabSystemUseHypervisor": "使用 Hypervisor 虚拟化", "MenuBarFile": "文件(_F)", "MenuBarFileOpenFromFile": "加载游戏文件(_L)", @@ -30,9 +31,13 @@ "MenuBarToolsInstallFirmware": "安装系统固件", "MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 文件中安装系统固件", "MenuBarFileToolsInstallFirmwareFromDirectory": "从文件夹中安装系统固件", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "管理文件扩展名", "MenuBarToolsInstallFileTypes": "关联文件扩展名", "MenuBarToolsUninstallFileTypes": "取消关联扩展名", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "视图(_V)", "MenuBarViewWindow": "窗口大小", "MenuBarViewWindow720": "720p", @@ -84,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "打开存放游戏 MOD 的目录", "GameListContextMenuOpenSdModsDirectory": "打开大气层系统 MOD 目录", "GameListContextMenuOpenSdModsDirectoryToolTip": "打开存放适用于大气层系统的游戏 MOD 的目录,对于为真实硬件打包的 MOD 非常有用", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} 游戏加载完成", "StatusBarSystemVersion": "系统固件版本:{0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "检测到操作系统内存映射最大数量被设置的过低", "LinuxVmMaxMapCountDialogTextPrimary": "你想要将操作系统 vm.max_map_count 的值增加到 {0} 吗", "LinuxVmMaxMapCountDialogTextSecondary": "有些游戏可能会尝试创建超过当前系统允许的内存映射最大数量,若超过当前最大数量,Ryujinx 模拟器将会闪退。", @@ -100,13 +108,14 @@ "SettingsTabGeneralCheckUpdatesOnLaunch": "启动时检查更新", "SettingsTabGeneralShowConfirmExitDialog": "退出游戏时需要确认", "SettingsTabGeneralRememberWindowState": "记住窗口大小和位置", - "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralShowTitleBar": "显示标题栏 (需要重启)", "SettingsTabGeneralHideCursor": "隐藏鼠标指针:", "SettingsTabGeneralHideCursorNever": "从不隐藏", "SettingsTabGeneralHideCursorOnIdle": "自动隐藏", "SettingsTabGeneralHideCursorAlways": "始终隐藏", "SettingsTabGeneralGameDirectories": "游戏目录", "SettingsTabGeneralAutoloadDirectories": "自动加载DLC/游戏更新目录", + "SettingsTabGeneralAutoloadNote": "DLC/游戏更新可自动加载和卸载", "SettingsTabGeneralAdd": "添加", "SettingsTabGeneralRemove": "删除", "SettingsTabSystem": "系统", @@ -141,7 +150,7 @@ "SettingsTabSystemSystemTime": "系统时钟:", "SettingsTabSystemEnableVsync": "启用垂直同步", "SettingsTabSystemEnablePptc": "开启 PPTC 缓存", - "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableLowPowerPptc": "低功耗 PPTC 加载", "SettingsTabSystemEnableFsIntegrityChecks": "启用文件系统完整性检查", "SettingsTabSystemAudioBackend": "音频处理引擎:", "SettingsTabSystemAudioBackendDummy": "无", @@ -399,6 +408,8 @@ "InputDialogTitle": "输入对话框", "InputDialogOk": "完成", "InputDialogCancel": "取消", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "选择用户名称", "InputDialogAddNewProfileHeader": "请输入账户名称", "InputDialogAddNewProfileSubtext": "(最大长度:{0})", @@ -406,6 +417,7 @@ "AvatarSetBackgroundColor": "设置背景色", "AvatarClose": "关闭", "ControllerSettingsLoadProfileToolTip": "加载配置文件", + "ControllerSettingsViewProfileToolTip": "预览配置文件", "ControllerSettingsAddProfileToolTip": "新增配置文件", "ControllerSettingsRemoveProfileToolTip": "删除配置文件", "ControllerSettingsSaveProfileToolTip": "保存配置文件", @@ -436,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "查找指定存档时出错:{0}", "FolderDialogExtractTitle": "选择要提取到的文件夹", "DialogNcaExtractionMessage": "提取 {1} 的 {0} 分区...", - "DialogNcaExtractionTitle": "Ryujinx - NCA 分区提取", + "DialogNcaExtractionTitle": "NCA 分区提取", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失败,所选文件中没有 NCA 文件", "DialogNcaExtractionCheckLogErrorMessage": "提取失败,请查看日志文件获取详情", "DialogNcaExtractionSuccessMessage": "提取成功!", @@ -449,12 +461,13 @@ "DialogUpdaterExtractionMessage": "正在提取更新...", "DialogUpdaterRenamingMessage": "正在重命名更新...", "DialogUpdaterAddingFilesMessage": "安装更新中...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "更新成功!", "DialogUpdaterRestartMessage": "是否立即重启 Ryujinx 模拟器?", "DialogUpdaterNoInternetMessage": "没有连接到网络", "DialogUpdaterNoInternetSubMessage": "请确保互联网连接正常。", "DialogUpdaterDirtyBuildMessage": "无法更新非官方版本的 Ryujinx 模拟器!", - "DialogUpdaterDirtyBuildSubMessage": "如果想使用受支持的版本,请您在 https://https://github.com/GreemDev/Ryujinx/releases/ 下载官方版本。", + "DialogUpdaterDirtyBuildSubMessage": "如果想使用受支持的版本,请您在 https://ryujinx.app/download 下载官方版本。", "DialogRestartRequiredMessage": "需要重启模拟器", "DialogThemeRestartMessage": "主题设置已保存,需要重启模拟器才能生效。", "DialogThemeRestartSubMessage": "是否要重启模拟器?", @@ -467,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "成功解除文件类型关联!", "DialogUninstallFileTypesErrorMessage": "解除文件类型关联失败!", "DialogOpenSettingsWindowLabel": "打开设置窗口", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "控制器小窗口", "DialogMessageDialogErrorExceptionMessage": "显示消息对话框时出错:{0}", "DialogSoftwareKeyboardErrorExceptionMessage": "显示软件键盘时出错:{0}", @@ -495,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n是否继续?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "安装系统固件中...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安装系统固件版本 {0} 。", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "删除后将没有可用的账户", "DialogUserProfileDeletionConfirmMessage": "是否删除所选账户", "DialogUserProfileUnsavedChangesTitle": "警告 - 有未保存的更改", @@ -665,16 +686,25 @@ "UserErrorUnknownDescription": "出现未知错误!", "UserErrorUndefinedDescription": "出现未定义错误!此类错误不应出现,请联系开发者!", "OpenSetupGuideMessage": "打开安装指南", - "NoUpdate": "无更新(或不加载游戏更新)", + "NoUpdate": "无更新(默认版本)", "TitleUpdateVersionLabel": "游戏更新的版本 {0}", "TitleBundledUpdateVersionLabel": "捆绑:版本 {0}", "TitleBundledDlcLabel": "捆绑:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - 信息", "RyujinxConfirm": "Ryujinx - 确认", "FileDialogAllTypes": "全部类型", "Never": "从不", "SwkbdMinCharacters": "不少于 {0} 个字符", "SwkbdMinRangeCharacters": "必须为 {0}-{1} 个字符", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "软键盘", "SoftwareKeyboardModeNumeric": "只能输入 0-9 或 \".\"", "SoftwareKeyboardModeAlphabet": "仅支持非中文字符", @@ -721,23 +751,53 @@ "SelectDlcDialogTitle": "选择 DLC 文件", "SelectUpdateDialogTitle": "选择更新文件", "SelectModDialogTitle": "选择 MOD 目录", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "管理用户账户", "CheatWindowTitle": "金手指管理器", "DlcWindowTitle": "管理 {0} ({1}) 的 DLC", "ModWindowTitle": "管理 {0} ({1}) 的 MOD", "UpdateWindowTitle": "游戏更新管理器", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", "UpdateWindowUpdateAddedMessage": "{0} 个更新被添加", - "UpdateWindowBundledContentNotice": "捆绑的更新无法被移除,只可被禁用。", + "UpdateWindowBundledContentNotice": "游戏整合的更新无法移除,可尝试禁用。", "CheatWindowHeading": "适用于 {0} [{1}] 的金手指", "BuildId": "游戏版本 ID:", - "DlcWindowBundledContentNotice": "捆绑的DLC无法被移除,只可被禁用。", + "DlcWindowBundledContentNotice": "游戏整合的DLC无法移除,可尝试禁用。", "DlcWindowHeading": "{0} 个 DLC", "DlcWindowDlcAddedMessage": "{0} 个DLC被添加", "AutoloadDlcAddedMessage": "{0} 个DLC被添加", + "AutoloadDlcRemovedMessage": "{0} 个失效的DLC已移除", "AutoloadUpdateAddedMessage": "{0} 个游戏更新被添加", - "AutoloadDlcAndUpdateAddedMessage": "{0} 个DLC和{1} 个游戏更新被添加", - "ModWindowHeading": "{0} Mod(s)", + "AutoloadUpdateRemovedMessage": "{0} 个失效的游戏更新已移除", + "ModWindowHeading": "{0} Mod", "UserProfilesEditProfile": "编辑所选", + "Continue": "Continue", "Cancel": "取消", "Save": "保存", "Discard": "放弃", @@ -765,7 +825,7 @@ "SettingsEnableMacroHLE": "启用 HLE 宏加速", "SettingsEnableMacroHLETooltip": "GPU 宏指令的高级模拟。\n\n提高性能表现,但一些游戏可能会出现图形错误。\n\n如果不确定,请保持开启状态。", "SettingsEnableColorSpacePassthrough": "色彩空间直通", - "SettingsEnableColorSpacePassthroughTooltip": "使 Vulkan 图形引擎直接传输原始色彩信息。对于宽色域 (例如 DCI-P3) 显示器的用户来说,可以产生更鲜艳的颜色,代价是会损失部分色彩准确度。", + "SettingsEnableColorSpacePassthroughTooltip": "使 Vulkan 图形引擎直接传输原始色彩信息。对于广色域 (例如 DCI-P3) 显示器的用户来说,可以产生更鲜艳的颜色,代价是损失部分色彩准确度。", "VolumeShort": "音量", "UserProfilesManageSaves": "管理存档", "DeleteUserSave": "确定删除此游戏的用户存档吗?", @@ -784,9 +844,9 @@ "GraphicsScalingFilterLabel": "缩放过滤:", "GraphicsScalingFilterTooltip": "选择在分辨率缩放时将使用的缩放过滤器。\n\nBilinear(双线性过滤)对于3D游戏效果较好,是一个安全的默认选项。\n\nNearest(最近邻过滤)推荐用于像素艺术游戏。\n\nFSR(超级分辨率锐画)只是一个锐化过滤器,不推荐与 FXAA 或 SMAA 抗锯齿一起使用。\n\nArea(局部过滤),当渲染分辨率大于窗口实际分辨率,推荐该选项。该选项在渲染比例大于2.0的情况下,可以实现超采样的效果。\n\n在游戏运行时,通过点击下面的“应用”按钮可以使设置生效;你可以将设置窗口移开,并试验找到您喜欢的游戏画面效果。\n\n如果不确定,请保持为“Bilinear(双线性过滤)”。", "GraphicsScalingFilterBilinear": "Bilinear(双线性过滤)", - "GraphicsScalingFilterNearest": "Nearest(最近邻过滤)", + "GraphicsScalingFilterNearest": "Nearest(邻近过滤)", "GraphicsScalingFilterFsr": "FSR(超级分辨率锐画技术)", - "GraphicsScalingFilterArea": "Area(局部过滤)", + "GraphicsScalingFilterArea": "Area(区域过滤)", "GraphicsScalingFilterLevelLabel": "等级", "GraphicsScalingFilterLevelTooltip": "设置 FSR 1.0 的锐化等级,数值越高,图像越锐利。", "SmaaLow": "SMAA 低质量", @@ -803,7 +863,19 @@ "AboutChangelogButtonTooltipMessage": "点击这里在浏览器中打开此版本的更新日志。", "SettingsTabNetworkMultiplayer": "多人联机游玩", "MultiplayerMode": "联机模式:", - "MultiplayerModeTooltip": "修改 LDN 多人联机游玩模式。\n\nldn_mitm 联机插件将修改游戏中的本地无线和本地游玩功能,使其表现得像局域网一样,允许和其他安装了 ldn_mitm 插件的 Ryujinx 模拟器和破解的任天堂 Switch 主机在同一网络下进行本地连接,实现多人联机游玩。\n\n多人联机游玩要求所有玩家必须运行相同的游戏版本(例如,任天堂明星大乱斗特别版 v13.0.1 无法与 v13.0.0 版本联机)。\n\n如果不确定,请保持为“禁用”。", + "MultiplayerModeTooltip": "修改 LDN 多人联机游玩模式。\n\nldn_mitm 联机插件将修改游戏中的本地无线和本地游玩功能,使其表现得像局域网一样,允许和其他安装了 ldn_mitm 插件的 Ryujinx 模拟器和破解的任天堂 Switch 主机在同一网络下进行本地连接,实现多人联机游玩。\n\n多人联机游玩要求所有玩家必须运行相同的游戏版本(例如,游戏版本 v13.0.1 无法与 v13.0.0 联机)。\n\n如果不确定,请保持为“禁用”。", "MultiplayerModeDisabled": "禁用", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json index e7cf35e5f..aaf8170c0 100644 --- a/src/Ryujinx/Assets/Locales/zh_TW.json +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -1,6 +1,7 @@ { "Language": "繁體中文 (台灣)", "MenuBarFileOpenApplet": "開啟小程式", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "在獨立模式下開啟 Mii 編輯器小程式", "SettingsTabInputDirectMouseAccess": "滑鼠直接存取", "SettingsTabSystemMemoryManagerMode": "記憶體管理員模式:", @@ -10,7 +11,10 @@ "SettingsTabSystemUseHypervisor": "使用 Hypervisor", "MenuBarFile": "檔案(_F)", "MenuBarFileOpenFromFile": "從檔案載入應用程式(_L)", + "MenuBarFileOpenFromFileError": "未能從已選擇的檔案中找到應用程式。", "MenuBarFileOpenUnpacked": "載入未封裝的遊戲(_U)", + "MenuBarFileLoadDlcFromFolder": "從資料夾中載入 DLC", + "MenuBarFileLoadTitleUpdatesFromFolder": "從資料夾中載入遊戲更新", "MenuBarFileOpenEmuFolder": "開啟 Ryujinx 資料夾", "MenuBarFileOpenLogsFolder": "開啟日誌資料夾", "MenuBarFileExit": "結束(_E)", @@ -27,9 +31,13 @@ "MenuBarToolsInstallFirmware": "安裝韌體", "MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體", "MenuBarFileToolsInstallFirmwareFromDirectory": "從資料夾安裝韌體", + "MenuBarToolsInstallKeys": "Install Keys", + "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", "MenuBarToolsManageFileTypes": "管理檔案類型", "MenuBarToolsInstallFileTypes": "安裝檔案類型", "MenuBarToolsUninstallFileTypes": "移除檔案類型", + "MenuBarToolsXCITrimmer": "Trim XCI Files", "MenuBarView": "檢視(_V)", "MenuBarViewWindow": "視窗大小", "MenuBarViewWindow720": "720p", @@ -81,8 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "開啟此應用程式模組的資料夾", "GameListContextMenuOpenSdModsDirectory": "開啟 Atmosphere 模組資料夾", "GameListContextMenuOpenSdModsDirectoryToolTip": "開啟此應用程式模組的另一個 SD 卡 Atmosphere 資料夾。適用於為真實硬體封裝的模組。", + "GameListContextMenuTrimXCI": "Check and Trim XCI File", + "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", "StatusBarGamesLoaded": "{0}/{1} 遊戲已載入", "StatusBarSystemVersion": "系統版本: {0}", + "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", "LinuxVmMaxMapCountDialogTitle": "檢測到記憶體映射的低限值", "LinuxVmMaxMapCountDialogTextPrimary": "您是否要將 vm.max_map_count 的數值增至 {0}?", "LinuxVmMaxMapCountDialogTextSecondary": "某些遊戲可能會嘗試建立超過目前允許的記憶體映射。一旦超過此限制,Ryujinx 就會崩潰。", @@ -97,12 +108,14 @@ "SettingsTabGeneralCheckUpdatesOnLaunch": "啟動時檢查更新", "SettingsTabGeneralShowConfirmExitDialog": "顯示「確認結束」對話方塊", "SettingsTabGeneralRememberWindowState": "記住視窗大小/位置", - "SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)", + "SettingsTabGeneralShowTitleBar": "顯示「標題列」 (需要重新開啟Ryujinx)", "SettingsTabGeneralHideCursor": "隱藏滑鼠游標:", "SettingsTabGeneralHideCursorNever": "從不", "SettingsTabGeneralHideCursorOnIdle": "閒置時", "SettingsTabGeneralHideCursorAlways": "總是", "SettingsTabGeneralGameDirectories": "遊戲資料夾", + "SettingsTabGeneralAutoloadDirectories": "自動載入 DLC/遊戲更新資料夾", + "SettingsTabGeneralAutoloadNote": "遺失的 DLC 及遊戲更新檔案將會在自動載入中移除", "SettingsTabGeneralAdd": "新增", "SettingsTabGeneralRemove": "刪除", "SettingsTabSystem": "系統", @@ -137,7 +150,7 @@ "SettingsTabSystemSystemTime": "系統時鐘:", "SettingsTabSystemEnableVsync": "垂直同步", "SettingsTabSystemEnablePptc": "PPTC (剖析式持久轉譯快取, Profiled Persistent Translation Cache)", - "SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC", + "SettingsTabSystemEnableLowPowerPptc": "低功耗 PPTC", "SettingsTabSystemEnableFsIntegrityChecks": "檔案系統完整性檢查", "SettingsTabSystemAudioBackend": "音效後端:", "SettingsTabSystemAudioBackendDummy": "虛設 (Dummy)", @@ -395,6 +408,8 @@ "InputDialogTitle": "輸入對話方塊", "InputDialogOk": "確定", "InputDialogCancel": "取消", + "InputDialogCancelling": "Cancelling", + "InputDialogClose": "Close", "InputDialogAddNewProfileTitle": "選擇設定檔名稱", "InputDialogAddNewProfileHeader": "請輸入設定檔名稱", "InputDialogAddNewProfileSubtext": "(最大長度: {0})", @@ -402,6 +417,7 @@ "AvatarSetBackgroundColor": "設定背景顏色", "AvatarClose": "關閉", "ControllerSettingsLoadProfileToolTip": "載入設定檔", + "ControllerSettingsViewProfileToolTip": "View Profile", "ControllerSettingsAddProfileToolTip": "新增設定檔", "ControllerSettingsRemoveProfileToolTip": "刪除設定檔", "ControllerSettingsSaveProfileToolTip": "儲存設定檔", @@ -411,6 +427,7 @@ "GameListContextMenuToggleFavorite": "加入/移除為我的最愛", "GameListContextMenuToggleFavoriteToolTip": "切換遊戲的我的最愛狀態", "SettingsTabGeneralTheme": "佈景主題:", + "SettingsTabGeneralThemeAuto": "自動", "SettingsTabGeneralThemeDark": "深色", "SettingsTabGeneralThemeLight": "淺色", "ControllerSettingsConfigureGeneral": "配置", @@ -431,7 +448,7 @@ "DialogMessageFindSaveErrorMessage": "尋找指定的存檔時出現錯誤: {0}", "FolderDialogExtractTitle": "選擇要解壓到的資料夾", "DialogNcaExtractionMessage": "從 {1} 提取 {0} 分區...", - "DialogNcaExtractionTitle": "Ryujinx - NCA 分區提取器", + "DialogNcaExtractionTitle": "NCA 分區提取器", "DialogNcaExtractionMainNcaNotFoundErrorMessage": "提取失敗。所選檔案中不存在主 NCA 檔案。", "DialogNcaExtractionCheckLogErrorMessage": "提取失敗。請閱讀日誌檔案了解更多資訊。", "DialogNcaExtractionSuccessMessage": "提取成功。", @@ -444,12 +461,13 @@ "DialogUpdaterExtractionMessage": "正在提取更新...", "DialogUpdaterRenamingMessage": "重新命名更新...", "DialogUpdaterAddingFilesMessage": "加入新更新...", + "DialogUpdaterShowChangelogMessage": "Show Changelog", "DialogUpdaterCompleteMessage": "更新成功!", "DialogUpdaterRestartMessage": "您現在要重新啟動 Ryujinx 嗎?", "DialogUpdaterNoInternetMessage": "您沒有連線到網際網路!", "DialogUpdaterNoInternetSubMessage": "請確認您的網際網路連線正常!", "DialogUpdaterDirtyBuildMessage": "您無法更新非官方版本的 Ryujinx!", - "DialogUpdaterDirtyBuildSubMessage": "如果您正在尋找受官方支援的版本,請從 https://https://github.com/GreemDev/Ryujinx/releases/ 下載 Ryujinx。", + "DialogUpdaterDirtyBuildSubMessage": "如果您正在尋找受官方支援的版本,請從 https://ryujinx.app/download 下載 Ryujinx。", "DialogRestartRequiredMessage": "需要重新啟動", "DialogThemeRestartMessage": "佈景主題設定已儲存。需要重新啟動才能套用主題。", "DialogThemeRestartSubMessage": "您要重新啟動嗎", @@ -462,6 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "成功移除檔案類型!", "DialogUninstallFileTypesErrorMessage": "無法移除檔案類型。", "DialogOpenSettingsWindowLabel": "開啟設定視窗", + "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", "DialogControllerAppletTitle": "控制器小程式", "DialogMessageDialogErrorExceptionMessage": "顯示訊息對話方塊時出現錯誤: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "顯示軟體鍵盤時出現錯誤: {0}", @@ -490,6 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n您確定要繼續嗎?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "正在安裝韌體...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "成功安裝系統版本 {0}。", + "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", + "DialogKeysInstallerKeysInstallTitle": "Install Keys", + "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", + "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", "DialogUserProfileDeletionWarningMessage": "如果刪除選取的設定檔,將無法開啟其他設定檔", "DialogUserProfileDeletionConfirmMessage": "您是否要刪除所選設定檔", "DialogUserProfileUnsavedChangesTitle": "警告 - 未儲存的變更", @@ -561,6 +587,9 @@ "AddGameDirBoxTooltip": "輸入要新增到清單中的遊戲資料夾", "AddGameDirTooltip": "新增遊戲資料夾到清單中", "RemoveGameDirTooltip": "移除選取的遊戲資料夾", + "AddAutoloadDirBoxTooltip": "輸入要新增到清單中的「自動載入 DLC/遊戲更新資料夾」", + "AddAutoloadDirTooltip": "新增「自動載入 DLC/遊戲更新資料夾」到清單中", + "RemoveAutoloadDirTooltip": "移除選取的「自動載入 DLC/遊戲更新資料夾」", "CustomThemeCheckTooltip": "為圖形使用者介面使用自訂 Avalonia 佈景主題,變更模擬器功能表的外觀", "CustomThemePathTooltip": "自訂 GUI 佈景主題的路徑", "CustomThemeBrowseTooltip": "瀏覽自訂 GUI 佈景主題", @@ -573,7 +602,7 @@ "TimeTooltip": "變更系統時鐘", "VSyncToggleTooltip": "模擬遊戲機的垂直同步。對大多數遊戲來說,它本質上是一個幀率限制器;停用它可能會導致遊戲以更高的速度執行,或使載入畫面耗時更長或卡住。\n\n可以在遊戲中使用快速鍵進行切換 (預設為 F1)。如果您打算停用,我們建議您這樣做。\n\n如果不確定,請保持開啟狀態。", "PptcToggleTooltip": "儲存已轉譯的 JIT 函數,這樣每次載入遊戲時就無需再轉譯這些函數。\n\n減少遊戲首次啟動後的卡頓現象,並大大加快啟動時間。\n\n如果不確定,請保持開啟狀態。", - "LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.", + "LowPowerPptcToggleTooltip": "使用 CPU 核心數量的三分之一載入 PPTC。", "FsIntegrityToggleTooltip": "在啟動遊戲時檢查損壞的檔案,如果檢測到損壞的檔案,則在日誌中顯示雜湊值錯誤。\n\n對效能沒有影響,旨在幫助排除故障。\n\n如果不確定,請保持開啟狀態。", "AudioBackendTooltip": "變更用於繪製音訊的後端。\n\nSDL2 是首選,而 OpenAL 和 SoundIO 則作為備用。虛設 (Dummy) 將沒有聲音。\n\n如果不確定,請設定為 SDL2。", "MemoryManagerTooltip": "變更客體記憶體的映射和存取方式。這會極大地影響模擬 CPU 效能。\n\n如果不確定,請設定為主體略過檢查模式。", @@ -581,7 +610,7 @@ "MemoryManagerHostTooltip": "直接映射主體位址空間中的記憶體。更快的 JIT 編譯和執行速度。", "MemoryManagerUnsafeTooltip": "直接映射記憶體,但在存取前不封鎖客體位址空間內的位址。速度更快,但相對不安全。訪客應用程式可以從 Ryujinx 中的任何地方存取記憶體,因此只能使用該模式執行您信任的程式。", "UseHypervisorTooltip": "使用 Hypervisor 取代 JIT。使用時可大幅提高效能,但在目前狀態下可能不穩定。", - "DRamTooltip": "利用另一種 MemoryMode 配置來模仿 Switch 開發模式。\n\n這僅對高解析度紋理套件或 4K 解析度模組有用。不會提高效能。\n\n如果不確定,請保持關閉狀態。", + "DRamTooltip": "利用另一種 MemoryMode 配置來模仿 Switch 開發模式。\n\n這僅對高解析度紋理套件或 4K 解析度模組有用。不會提高效能。\n\n如果不確定,請設定為 4GiB。", "IgnoreMissingServicesTooltip": "忽略未實現的 Horizon OS 服務。這可能有助於在啟動某些遊戲時避免崩潰。\n\n如果不確定,請保持關閉狀態。", "IgnoreAppletTooltip": "如果遊戲手把在遊戲過程中斷開連接,則外部對話方塊「控制器小程式」將不會出現。不會提示關閉對話方塊或設定新控制器。一旦先前斷開的控制器重新連接,遊戲將自動恢復。", "GraphicsBackendThreadingTooltip": "在第二個執行緒上執行圖形後端指令。\n\n在本身不支援多執行緒的 GPU 驅動程式上,可加快著色器編譯、減少卡頓並提高效能。在支援多執行緒的驅動程式上效能略有提升。\n\n如果不確定,請設定為自動。", @@ -606,6 +635,8 @@ "DebugLogTooltip": "在控制台中輸出偵錯日誌訊息。\n\n只有在人員特別指示的情況下才能使用,因為這會導致日誌難以閱讀,並降低模擬器效能。", "LoadApplicationFileTooltip": "開啟檔案總管,選擇與 Switch 相容的檔案來載入", "LoadApplicationFolderTooltip": "開啟檔案總管,選擇與 Switch 相容且未封裝的應用程式來載入", + "LoadDlcFromFolderTooltip": "開啟檔案總管,選擇一個或多個資料夾來大量載入 DLC", + "LoadTitleUpdatesFromFolderTooltip": "開啟檔案總管,選擇一個或多個資料夾來大量載入遊戲更新", "OpenRyujinxFolderTooltip": "開啟 Ryujinx 檔案系統資料夾", "OpenRyujinxLogsTooltip": "開啟日誌被寫入的資料夾", "ExitTooltip": "結束 Ryujinx", @@ -657,12 +688,23 @@ "OpenSetupGuideMessage": "開啟設定指南", "NoUpdate": "沒有更新", "TitleUpdateVersionLabel": "版本 {0}", + "TitleBundledUpdateVersionLabel": "附帶: 版本 {0}", + "TitleBundledDlcLabel": "附帶:", + "TitleXCIStatusPartialLabel": "Partial", + "TitleXCIStatusTrimmableLabel": "Untrimmed", + "TitleXCIStatusUntrimmableLabel": "Trimmed", + "TitleXCIStatusFailedLabel": "(Failed)", + "TitleXCICanSaveLabel": "Save {0:n0} Mb", + "TitleXCISavingLabel": "Saved {0:n0} Mb", "RyujinxInfo": "Ryujinx - 資訊", "RyujinxConfirm": "Ryujinx - 確認", "FileDialogAllTypes": "全部類型", "Never": "從不", "SwkbdMinCharacters": "長度必須至少為 {0} 個字元", "SwkbdMinRangeCharacters": "長度必須為 {0} 到 {1} 個字元", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "軟體鍵盤", "SoftwareKeyboardModeNumeric": "必須是 0 到 9 或「.」", "SoftwareKeyboardModeAlphabet": "必須是「非中日韓字元」 (non CJK)", @@ -709,16 +751,53 @@ "SelectDlcDialogTitle": "選取 DLC 檔案", "SelectUpdateDialogTitle": "選取更新檔", "SelectModDialogTitle": "選取模組資料夾", + "TrimXCIFileDialogTitle": "Check and Trim XCI File", + "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", + "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", + "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", + "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", + "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", + "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", + "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", + "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", + "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", + "TrimXCIFileCancelled": "The operation was cancelled", + "TrimXCIFileFileUndertermined": "No operation was performed", "UserProfileWindowTitle": "使用者設定檔管理員", "CheatWindowTitle": "密技管理員", "DlcWindowTitle": "管理 {0} 的可下載內容 ({1})", "ModWindowTitle": "管理 {0} 的模組 ({1})", "UpdateWindowTitle": "遊戲更新管理員", + "XCITrimmerWindowTitle": "XCI File Trimmer", + "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", + "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", + "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", + "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", + "XCITrimmerTitleStatusFailed": "Failed", + "XCITrimmerPotentialSavings": "Potential Savings", + "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerSavingsMb": "{0:n0} Mb", + "XCITrimmerSelectDisplayed": "Select Shown", + "XCITrimmerDeselectDisplayed": "Deselect Shown", + "XCITrimmerSortName": "Title", + "XCITrimmerSortSaved": "Space Savings", + "XCITrimmerTrim": "Trim", + "XCITrimmerUntrim": "Untrim", + "UpdateWindowUpdateAddedMessage": "已加入 {0} 個遊戲更新", + "UpdateWindowBundledContentNotice": "附帶的遊戲更新只能被停用而無法被刪除。", "CheatWindowHeading": "可用於 {0} [{1}] 的密技", "BuildId": "組建識別碼:", + "DlcWindowBundledContentNotice": "附帶的 DLC 只能被停用而無法被刪除。", "DlcWindowHeading": "{0} 個可下載內容", + "DlcWindowDlcAddedMessage": "已加入 {0} 個 DLC", + "AutoloadDlcAddedMessage": "已加入 {0} 個 DLC", + "AutoloadDlcRemovedMessage": "已刪除 {0} 個遺失的 DLC", + "AutoloadUpdateAddedMessage": "已加入 {0} 個遊戲更新", + "AutoloadUpdateRemovedMessage": "已刪除 {0} 個遺失的遊戲更新", "ModWindowHeading": "{0} 模組", "UserProfilesEditProfile": "編輯所選", + "Continue": "Continue", "Cancel": "取消", "Save": "儲存", "Discard": "放棄變更", @@ -767,6 +846,7 @@ "GraphicsScalingFilterBilinear": "雙線性 (Bilinear)", "GraphicsScalingFilterNearest": "近鄰性 (Nearest)", "GraphicsScalingFilterFsr": "FSR", + "GraphicsScalingFilterArea": "Area", "GraphicsScalingFilterLevelLabel": "日誌等級", "GraphicsScalingFilterLevelTooltip": "設定 FSR 1.0 銳化等級。越高越清晰。", "SmaaLow": "低階 SMAA", @@ -785,5 +865,17 @@ "MultiplayerMode": "模式:", "MultiplayerModeTooltip": "變更 LDN 多人遊戲模式。\n\nLdnMitm 將修改遊戲中的本機無線/本機遊戲功能,使其如同區域網路一樣執行,允許與其他安裝了 ldn_mitm 模組的 Ryujinx 實例和已破解的 Nintendo Switch 遊戲機進行本機同網路連線。\n\n多人遊戲要求所有玩家使用相同的遊戲版本 (例如,Super Smash Bros. Ultimate v13.0.1 無法連接 v13.0.0)。\n\n如果不確定,請保持 Disabled (停用) 狀態。", "MultiplayerModeDisabled": "已停用", - "MultiplayerModeLdnMitm": "ldn_mitm" + "MultiplayerModeLdnMitm": "ldn_mitm", + "MultiplayerModeLdnRyu": "RyuLDN", + "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)", + "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.", + "LdnPassphrase": "Network Passphrase:", + "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.", + "LdnPassphraseInputPublic": "(public)", + "GenLdnPass": "Generate Random", + "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.", + "ClearLdnPass": "Clear", + "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.", + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/Assets/Styles/Styles.xaml b/src/Ryujinx/Assets/Styles/Styles.xaml index b3a6f59c8..878b5e7f1 100644 --- a/src/Ryujinx/Assets/Styles/Styles.xaml +++ b/src/Ryujinx/Assets/Styles/Styles.xaml @@ -1,7 +1,8 @@  + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia"> @@ -43,6 +44,10 @@ + - + + + + + + + + + + + + + + Dispatcher.UIThread.Post(() => { if (Window.ViewModel.EnableNonGameRunningControls) - Refresh_OnClick(null, null); + Window.LoadApplications(); }); } } - private void VsyncStatus_PointerReleased(object sender, PointerReleasedEventArgs e) + private void VSyncMode_PointerReleased(object sender, PointerReleasedEventArgs e) { - Window.ViewModel.AppHost.ToggleVSync(); - - Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {Window.ViewModel.AppHost.Device.EnableDeviceVsync}"); + Window.ViewModel.ToggleVSyncMode(); + Logger.Info?.Print(LogClass.Application, $"VSync Mode toggled to: {Window.ViewModel.AppHost.Device.VSyncMode}"); } private void DockedStatus_PointerReleased(object sender, PointerReleasedEventArgs e) diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml index 06f6728da..da0957e02 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml @@ -1,4 +1,4 @@ - - - - + + + @@ -103,6 +103,18 @@ + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs index fb0fe2bb1..609f61633 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs @@ -82,8 +82,8 @@ namespace Ryujinx.Ava.UI.Views.Settings switch (button.Name) { - case "ToggleVsync": - viewModel.KeyboardHotkey.ToggleVsync = buttonValue.AsHidType(); + case "ToggleVSyncMode": + viewModel.KeyboardHotkey.ToggleVSyncMode = buttonValue.AsHidType(); break; case "Screenshot": viewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType(); @@ -109,6 +109,12 @@ namespace Ryujinx.Ava.UI.Views.Settings case "VolumeDown": viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType(); break; + case "CustomVSyncIntervalIncrement": + viewModel.KeyboardHotkey.CustomVSyncIntervalIncrement = buttonValue.AsHidType(); + break; + case "CustomVSyncIntervalDecrement": + viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = buttonValue.AsHidType(); + break; } } }; diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml index c7736bf8d..2fc59f04d 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs index b771933eb..c69307522 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs @@ -1,12 +1,29 @@ using Avalonia.Controls; +using Avalonia.Interactivity; +using Ryujinx.Ava.UI.ViewModels; +using System; namespace Ryujinx.Ava.UI.Views.Settings { public partial class SettingsNetworkView : UserControl { + public SettingsViewModel ViewModel; + public SettingsNetworkView() { InitializeComponent(); } + + private void GenLdnPassButton_OnClick(object sender, RoutedEventArgs e) + { + byte[] code = new byte[4]; + new Random().NextBytes(code); + ViewModel.LdnPassphrase = $"Ryujinx-{BitConverter.ToUInt32(code):x8}"; + } + + private void ClearLdnPassButton_OnClick(object sender, RoutedEventArgs e) + { + ViewModel.LdnPassphrase = ""; + } } } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index 4fe57b425..e04e541c3 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -4,6 +4,7 @@ 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:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" @@ -181,11 +182,68 @@ Width="350" ToolTip.Tip="{ext:Locale TimeTooltip}" /> - + - + VerticalAlignment="Center" + Text="{ext:Locale SettingsTabSystemVSyncMode}" + ToolTip.Tip="{ext:Locale SettingsTabSystemVSyncModeTooltip}" + Width="250" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs b/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs index b4f23b5b8..dba762972 100644 --- a/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs +++ b/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs @@ -63,8 +63,7 @@ namespace Ryujinx.Ava.UI.Views.User private async void Import_OnClick(object sender, RoutedEventArgs e) { - var window = this.GetVisualRoot() as Window; - var result = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + var result = await ((Window)this.GetVisualRoot()!).StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { AllowMultiple = false, FileTypeFilter = new List diff --git a/src/Ryujinx/UI/Windows/AboutWindow.axaml b/src/Ryujinx/UI/Windows/AboutWindow.axaml index 6d4a7b7e3..1d0e36ae9 100644 --- a/src/Ryujinx/UI/Windows/AboutWindow.axaml +++ b/src/Ryujinx/UI/Windows/AboutWindow.axaml @@ -6,8 +6,10 @@ xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModel="clr-namespace:Ryujinx.Ava.UI.ViewModels" - Width="550" - Height="260" + MinWidth="550" + MinHeight="260" + MaxWidth="600" + MaxHeight="500" Margin="0,-12,0,0" d:DesignHeight="260" d:DesignWidth="550" diff --git a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs index edca7949a..8c8d56b34 100644 --- a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs @@ -75,7 +75,7 @@ namespace Ryujinx.Ava.UI.Windows string parentPath = currentCheatFile.Replace(titleModsPath, string.Empty); buildId = Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper(); - currentGroup = new CheatNode("", buildId, parentPath, true); + currentGroup = new CheatNode(string.Empty, buildId, parentPath, true); LoadedCheats.Add(currentGroup); } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 8a6be3c81..059f99a60 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -28,6 +28,7 @@ using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Helper; using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Linq; using System.Runtime.Versioning; using System.Threading; @@ -37,6 +38,8 @@ namespace Ryujinx.Ava.UI.Windows { public partial class MainWindow : StyleableAppWindow { + internal static MainWindowViewModel MainWindowViewModel { get; private set; } + public MainWindowViewModel ViewModel { get; } internal readonly AvaHostUIHandler UiHandler; @@ -64,12 +67,15 @@ namespace Ryujinx.Ava.UI.Windows public static bool ShowKeyErrorOnLoad { get; set; } public ApplicationLibrary ApplicationLibrary { get; set; } + // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) + public readonly double TitleBarHeight; + public readonly double StatusBarHeight; public readonly double MenuBarHeight; public MainWindow() { - DataContext = ViewModel = new MainWindowViewModel + DataContext = ViewModel = MainWindowViewModel = new MainWindowViewModel { Window = this }; @@ -84,12 +90,12 @@ namespace Ryujinx.Ava.UI.Windows TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowTitleBar; TitleBar.TitleBarHitTestType = (ConfigurationState.Instance.ShowTitleBar) ? TitleBarHitTestType.Simple : TitleBarHitTestType.Complex; + // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) + TitleBarHeight = (ConfigurationState.Instance.ShowTitleBar ? TitleBar.Height : 0); + // NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point. StatusBarHeight = StatusBarView.StatusBar.MinHeight; MenuBarHeight = MenuBar.MinHeight; - double barHeight = MenuBarHeight + StatusBarHeight; - Height = ((Height - barHeight) / Program.WindowScaleFactor) + barHeight; - Width /= Program.WindowScaleFactor; SetWindowSizePosition(); @@ -153,6 +159,36 @@ namespace Ryujinx.Ava.UI.Windows }); } + private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e) + { + Dispatcher.UIThread.Post(() => + { + var ldnGameDataArray = e.LdnData; + ViewModel.LastLdnGameData = ldnGameDataArray; + foreach (var application in ViewModel.Applications) + { + UpdateApplicationWithLdnData(application); + } + ViewModel.RefreshView(); + }); + } + + private void UpdateApplicationWithLdnData(ApplicationData application) + { + if (application.ControlHolder.ByteSpan.Length > 0 && ViewModel.LastLdnGameData != null) + { + IEnumerable ldnGameData = ViewModel.LastLdnGameData.Where(game => application.ControlHolder.Value.LocalCommunicationId.Items.Contains(Convert.ToUInt64(game.TitleId, 16))); + + application.PlayerCount = ldnGameData.Sum(game => game.PlayerCount); + application.GameCount = ldnGameData.Count(); + } + else + { + application.PlayerCount = 0; + application.GameCount = 0; + } + } + public void Application_Opened(object sender, ApplicationOpenedEventArgs args) { if (args.Application != null) @@ -349,12 +385,12 @@ namespace Ryujinx.Ava.UI.Windows await Dispatcher.UIThread.InvokeAsync(async () => await UserErrorDialog.ShowUserErrorDialog(UserError.NoKeys)); } - if (ConfigurationState.Instance.CheckUpdatesOnStart && Updater.CanUpdate(false)) + if (ConfigurationState.Instance.CheckUpdatesOnStart && !CommandLineState.HideAvailableUpdates && Updater.CanUpdate()) { - await Updater.BeginParse(this, false).ContinueWith(task => - { - Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"); - }, TaskContinuationOptions.OnlyOnFaulted); + await this.BeginUpdateAsync() + .ContinueWith( + task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"), + TaskContinuationOptions.OnlyOnFaulted); } } @@ -375,7 +411,8 @@ namespace Ryujinx.Ava.UI.Windows { if (!ConfigurationState.Instance.RememberWindowState) { - ViewModel.WindowHeight = (720 + StatusBarHeight + MenuBarHeight) * Program.WindowScaleFactor; + // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) + ViewModel.WindowHeight = (720 + StatusBarHeight + MenuBarHeight + TitleBarHeight) * Program.WindowScaleFactor; ViewModel.WindowWidth = 1280 * Program.WindowScaleFactor; WindowState = WindowState.Normal; @@ -392,30 +429,17 @@ namespace Ryujinx.Ava.UI.Windows ViewModel.WindowState = ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value ? WindowState.Maximized : WindowState.Normal; - if (CheckScreenBounds(savedPoint)) + if (Screens.All.Any(screen => screen.Bounds.Contains(savedPoint))) { Position = savedPoint; } else { + Logger.Warning?.Print(LogClass.Application, "Failed to find valid start-up coordinates. Defaulting to primary monitor center."); WindowStartupLocation = WindowStartupLocation.CenterScreen; } } - private bool CheckScreenBounds(PixelPoint configPoint) - { - for (int i = 0; i < Screens.ScreenCount; i++) - { - if (Screens.All[i].Bounds.Contains(configPoint)) - { - return true; - } - } - - Logger.Warning?.Print(LogClass.Application, "Failed to find valid start-up coordinates. Defaulting to primary monitor center."); - return false; - } - private void SaveWindowSizePosition() { ConfigurationState.Instance.UI.WindowStartup.WindowMaximized.Value = WindowState == WindowState.Maximized; @@ -423,8 +447,10 @@ namespace Ryujinx.Ava.UI.Windows // Only save rectangle properties if the window is not in a maximized state. if (WindowState != WindowState.Maximized) { - ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)Height; - ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)Width; + // Since scaling is being applied to the loaded settings from disk (see SetWindowSizePosition() above), scaling should be removed from width/height before saving out to disk + // as well - otherwise anyone not using a 1.0 scale factor their window will increase in size with every subsequent launch of the program when scaling is applied (Nov. 14, 2024) + ConfigurationState.Instance.UI.WindowStartup.WindowSizeHeight.Value = (int)(Height / Program.WindowScaleFactor); + ConfigurationState.Instance.UI.WindowStartup.WindowSizeWidth.Value = (int)(Width / Program.WindowScaleFactor); ConfigurationState.Instance.UI.WindowStartup.WindowPositionX.Value = Position.X; ConfigurationState.Instance.UI.WindowStartup.WindowPositionY.Value = Position.Y; @@ -462,7 +488,20 @@ namespace Ryujinx.Ava.UI.Windows .Connect() .ObserveOn(SynchronizationContext.Current!) .Bind(ViewModel.Applications) + .OnItemAdded(UpdateApplicationWithLdnData) .Subscribe(); + ApplicationLibrary.LdnGameDataReceived += ApplicationLibrary_LdnGameDataReceived; + + ConfigurationState.Instance.Multiplayer.Mode.Event += (sender, evt) => + { + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + }; + + ConfigurationState.Instance.Multiplayer.LdnServer.Event += (sender, evt) => + { + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + }; + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); ViewModel.RefreshFirmwareStatus(); @@ -471,7 +510,7 @@ namespace Ryujinx.Ava.UI.Windows { LoadApplications(); } - + _ = CheckLaunchState(); } @@ -507,8 +546,7 @@ namespace Ryujinx.Ava.UI.Windows private void VolumeStatus_CheckedChanged(object sender, RoutedEventArgs e) { - var volumeSplitButton = sender as ToggleSplitButton; - if (ViewModel.IsGameRunning) + if (ViewModel.IsGameRunning && sender is ToggleSplitButton volumeSplitButton) { if (!volumeSplitButton.IsChecked) { @@ -601,13 +639,26 @@ namespace Ryujinx.Ava.UI.Windows { switch (fileType) { - case "NSP": ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle(); break; - case "PFS0": ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle(); break; - case "XCI": ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle(); break; - case "NCA": ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle(); break; - case "NRO": ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle(); break; - case "NSO": ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle(); break; - default: throw new ArgumentOutOfRangeException(fileType); + case "NSP": + ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle(); + break; + case "PFS0": + ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle(); + break; + case "XCI": + ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle(); + break; + case "NCA": + ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle(); + break; + case "NRO": + ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle(); + break; + case "NSO": + ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle(); + break; + default: + throw new ArgumentOutOfRangeException(fileType); } ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml index f9d10fe4f..2bf5b55e7 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -109,7 +109,6 @@ HorizontalAlignment="Right" ReverseOrder="{Binding IsMacOS}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs new file mode 100644 index 000000000..6df862283 --- /dev/null +++ b/src/Ryujinx/UI/Windows/XCITrimmerWindow.axaml.cs @@ -0,0 +1,101 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Styling; +using FluentAvalonia.UI.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.UI.Common.Models; +using System; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.Windows +{ + public partial class XCITrimmerWindow : UserControl + { + public XCITrimmerViewModel ViewModel; + + public XCITrimmerWindow() + { + DataContext = this; + + InitializeComponent(); + } + + public XCITrimmerWindow(MainWindowViewModel mainWindowViewModel) + { + DataContext = ViewModel = new XCITrimmerViewModel(mainWindowViewModel); + + InitializeComponent(); + } + + public static async Task Show(MainWindowViewModel mainWindowViewModel) + { + ContentDialog contentDialog = new() + { + PrimaryButtonText = string.Empty, + SecondaryButtonText = string.Empty, + CloseButtonText = string.Empty, + Content = new XCITrimmerWindow(mainWindowViewModel), + Title = string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerWindowTitle]), + }; + + Style bottomBorder = new(x => x.OfType().Name("DialogSpace").Child().OfType()); + bottomBorder.Setters.Add(new Setter(IsVisibleProperty, false)); + + contentDialog.Styles.Add(bottomBorder); + + await contentDialog.ShowAsync(); + } + + private void Trim(object sender, RoutedEventArgs e) + { + ViewModel.TrimSelected(); + } + + private void Untrim(object sender, RoutedEventArgs e) + { + ViewModel.UntrimSelected(); + } + + private void Close(object sender, RoutedEventArgs e) + { + ((ContentDialog)Parent).Hide(); + } + + private void Cancel(Object sender, RoutedEventArgs e) + { + ViewModel.Cancel = true; + } + + public void Sort_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton { Tag: string sortField }) + ViewModel.SortingField = Enum.Parse(sortField); + } + + public void Order_Checked(object sender, RoutedEventArgs args) + { + if (sender is RadioButton { Tag: string sortOrder }) + ViewModel.SortingAscending = sortOrder is "Ascending"; + } + + private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + foreach (var content in e.AddedItems) + { + if (content is XCITrimmerFileModel applicationData) + { + ViewModel.Select(applicationData); + } + } + + foreach (var content in e.RemovedItems) + { + if (content is XCITrimmerFileModel applicationData) + { + ViewModel.Deselect(applicationData); + } + } + } + } +} diff --git a/src/Ryujinx/Updater.cs b/src/Ryujinx/Updater.cs index e8ef02052..bdb44d668 100644 --- a/src/Ryujinx/Updater.cs +++ b/src/Ryujinx/Updater.cs @@ -32,6 +32,9 @@ namespace Ryujinx.Ava internal static class Updater { private const string GitHubApiUrl = "https://api.github.com"; + private const string LatestReleaseUrl = + $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest"; + private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory; @@ -46,9 +49,9 @@ namespace Ryujinx.Ava private static bool _updateSuccessful; private static bool _running; - private static readonly string[] _windowsDependencyDirs = Array.Empty(); + private static readonly string[] _windowsDependencyDirs = []; - public static async Task BeginParse(Window mainWindow, bool showVersionUpToDate) + public static async Task BeginUpdateAsync(this Window mainWindow, bool showVersionUpToDate = false) { if (_running) { @@ -81,7 +84,7 @@ namespace Ryujinx.Ava } catch { - Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!"); + Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {App.FullAppName} version!"); await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage], @@ -96,11 +99,10 @@ namespace Ryujinx.Ava try { using HttpClient jsonClient = ConstructHttpClient(); - - string buildInfoUrl = $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest"; - string fetchedJson = await jsonClient.GetStringAsync(buildInfoUrl); + + string fetchedJson = await jsonClient.GetStringAsync(LatestReleaseUrl); var fetched = JsonHelper.Deserialize(fetchedJson, _serializerContext.GithubReleasesJsonResponse); - _buildVer = fetched.Name; + _buildVer = fetched.TagName; foreach (var asset in fetched.Assets) { @@ -112,9 +114,14 @@ namespace Ryujinx.Ava { if (showVersionUpToDate) { - await ContentDialogHelper.CreateUpdaterInfoDialog( + UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], string.Empty); + + if (userResult is UserResult.Ok) + { + OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion)); + } } _running = false; @@ -131,9 +138,14 @@ namespace Ryujinx.Ava { if (showVersionUpToDate) { - await ContentDialogHelper.CreateUpdaterInfoDialog( + UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], string.Empty); + + if (userResult is UserResult.Ok) + { + OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion)); + } } _running = false; @@ -159,7 +171,7 @@ namespace Ryujinx.Ava } catch { - Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from Github!"); + Logger.Error?.Print(LogClass.Application, $"Failed to convert the received {App.FullAppName} version from GitHub!"); await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage], @@ -174,9 +186,14 @@ namespace Ryujinx.Ava { if (showVersionUpToDate) { - await ContentDialogHelper.CreateUpdaterInfoDialog( + UserResult userResult = await ContentDialogHelper.CreateUpdaterUpToDateInfoDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterAlreadyOnLatestVersionMessage], string.Empty); + + if (userResult is UserResult.Ok) + { + OpenHelper.OpenUrl(ReleaseInformation.GetChangelogForVersion(currentVersion)); + } } _running = false; @@ -204,19 +221,29 @@ namespace Ryujinx.Ava await Dispatcher.UIThread.InvokeAsync(async () => { + string newVersionString = ReleaseInformation.IsCanaryBuild + ? $"Canary {currentVersion} -> Canary {newVersion}" + : $"{currentVersion} -> {newVersion}"; + + RequestUserToUpdate: // Show a message asking the user if they want to update - var shouldUpdate = await ContentDialogHelper.CreateChoiceDialog( + UserResult shouldUpdate = await ContentDialogHelper.CreateUpdaterChoiceDialog( LocaleManager.Instance[LocaleKeys.RyujinxUpdater], LocaleManager.Instance[LocaleKeys.RyujinxUpdaterMessage], - $"{Program.Version} -> {newVersion}"); + newVersionString); - if (shouldUpdate) + switch (shouldUpdate) { - await UpdateRyujinx(mainWindow, _buildUrl); - } - else - { - _running = false; + case UserResult.Yes: + await UpdateRyujinx(mainWindow, _buildUrl); + break; + // Secondary button maps to no, which in this case is the show changelog button. + case UserResult.No: + OpenHelper.OpenUrl(ReleaseInformation.GetChangelogUrl(currentVersion, newVersion)); + goto RequestUserToUpdate; + default: + _running = false; + break; } }); } @@ -636,7 +663,7 @@ namespace Ryujinx.Ava taskDialog.Hide(); } - public static bool CanUpdate(bool showWarnings) + public static bool CanUpdate(bool showWarnings = false) { #if !DISABLE_UPDATER if (!NetworkInterface.GetIsNetworkAvailable()) diff --git a/src/Spv.Generator/Spv.Generator.csproj b/src/Spv.Generator/Spv.Generator.csproj index ae2821edb..5dec0b64e 100644 --- a/src/Spv.Generator/Spv.Generator.csproj +++ b/src/Spv.Generator/Spv.Generator.csproj @@ -2,6 +2,7 @@ net8.0 + $(DefaultItemExcludes);._*