mirror of
https://github.com/GreemDev/Ryujinx.git
synced 2025-01-03 18:41:58 +00:00
v1.2.73 (#231)
Significant changes include LDN functionality from @Vudjun (no more separate build!) and an XCI trimmer from @amurgshere. Merged PRs in this release (in the order they were merged): #183, #150, #105, #160, #188, #98, #158, #13, #216, #73, #217, #122, #228, #65, #226, #236, #247, #243, #249, #242, #260, #273, #272, #262, #259, #241 ## Versioning: There now exists "stable" (release branch) and ["canary" (master branch)](https://github.com/GreemDev/Ryujinx-Canary/releases) versions. Instead of everyone using the same emulator, getting updates for every code change, you now *opt-in* to the more frequent updates by using the Canary version. Use stable and you'll get about an update a week, but that update will be MUCH more significant as it's the entire previous week's changes & PR merges. ## LDN LDN functionality is now merged! Use [this](https://github.com/GreemDev/Ryujinx/wiki/Multiplayer%E2%80%90(LDN%E2%80%90Local%E2%80%90Wireless)%E2%80%90Guide) to get started. Please note that LDN is only for local wireless; **this is not a Nintendo Switch Online emulation feature**. ## UI - Added an XCI trimmer (#105). - You can use this feature to trim dead bytes & the embedded firmware out of your dumped XCIs, to make them smaller. - If you right-click an XCI and the trim button it is greyed out, that means your XCI is already as small as possible. - Fix for fullscreen not being really fullscreen (#150) - Fix window sizing calculations when Show Title Bar is enabled (#247) - The "Install/Uninstall file types" buttons will be enabled/disabled depending on which one you contextually need; install will be clickable when they aren't installed, and vice versa. - Fix for showing default config screen when swapping players in controller settings (#122) - Command-line argument to prevent update checking `--hide-updates` (#272) - # RPC: - Added a LOT of game images to Discord RPC. - Play time will now show the time unit hours at a maximum. ## Localization - Update outdated/incorrect & added missing translations for zh-TW (#158) - Add many missing locale strings to all languages (#160) - Update & improve Korean translation (#226) - Minor fixes & add missing translations to Spanish translation (#242) ## Headless - Added `ignore-controller-applet` as an option you can configure via headless command-line options. ## Graphics Backend - ### Vulkan - fix divide-by-zero when recovering from missed draw (#235) - fixes crash in 'Baldo: The Guardian Owls' opening cutscene ## Horizon - fix crash that occurs when launching an NSP forwarder generated by Nro2Nsp (#237) # Nerd Zone Slightly more technical information. If you don't understand what's under here, no worry. - Updater now uses the release's Tag Name instead of its Name for version checking. - Baked in value change logging into ReactiveObject. - Split ConfigurationState into 3, smaller partial classes of the same name. - Specify if the current version is Canary in the version log line --------- Co-authored-by: James Duarte <GarnetSunset@users.noreply.github.com> Co-authored-by: Luke Warner <65521430+LukeWarnut@users.noreply.github.com> Co-authored-by: TheToid <amurgshere@gmail.com> Co-authored-by: GabCoolGuy <gabrielfreville@proton.me> Co-authored-by: Kekschen <52585984+Kek5chen@users.noreply.github.com> Co-authored-by: WilliamWsyHK <WilliamWsyHK@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jacobwasbeast <38381609+Jacobwasbeast@users.noreply.github.com> Co-authored-by: Piplup <100526773+piplup55@users.noreply.github.com> Co-authored-by: Vladimir Sokolov <tehnicalmailone@gmail.com> Co-authored-by: Jonas Henriksson <gr3ger@gmail.com> Co-authored-by: Vudjun <Vudjun@users.noreply.github.com> Co-authored-by: extherian <extherian@gmail.com> Co-authored-by: Hack茶ん <120134269+Hackjjang@users.noreply.github.com> Co-authored-by: EmulationEnjoyer <144477224+EmulationEnjoyer@users.noreply.github.com> Co-authored-by: Nicola <61830443+nicola02nb@users.noreply.github.com> Co-authored-by: jzumaran <juan.zumaran@gitz.cl> Co-authored-by: Pitchoune <yrigaud@icloud.com> Co-authored-by: Narugakuruga <31060534+Narugakuruga@users.noreply.github.com>
This commit is contained in:
parent
8a064bcd7e
commit
b2aad0a0fc
183 changed files with 10046 additions and 2661 deletions
60
.github/workflows/build.yml
vendored
60
.github/workflows/build.yml
vendored
|
@ -74,36 +74,36 @@ jobs:
|
||||||
chmod +x ./publish_sdl2_headless/Ryujinx.Headless.SDL2 ./publish_sdl2_headless/Ryujinx.sh
|
chmod +x ./publish_sdl2_headless/Ryujinx.Headless.SDL2 ./publish_sdl2_headless/Ryujinx.sh
|
||||||
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
||||||
|
|
||||||
#- name: Build AppImage
|
- name: Build AppImage
|
||||||
# if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
||||||
# run: |
|
run: |
|
||||||
# PLATFORM_NAME="${{ matrix.platform.name }}"
|
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
|
mkdir -p tools
|
||||||
# export PATH="$PATH:$(readlink -f tools)"
|
export PATH="$PATH:$(readlink -f tools)"
|
||||||
|
|
||||||
# # Setup appimagetool
|
# Setup appimagetool
|
||||||
# wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
wget -q -O tools/appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||||
# chmod +x tools/appimagetool
|
chmod +x tools/appimagetool
|
||||||
# chmod +x distribution/linux/appimage/build-appimage.sh
|
chmod +x distribution/linux/appimage/build-appimage.sh
|
||||||
|
|
||||||
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
|
# Explicitly set $ARCH for appimagetool ($ARCH_NAME is for the file name)
|
||||||
# if [ "$PLATFORM_NAME" = "linux-x64" ]; then
|
if [ "$PLATFORM_NAME" = "linux-x64" ]; then
|
||||||
# ARCH_NAME=x64
|
ARCH_NAME=x64
|
||||||
# export ARCH=x86_64
|
export ARCH=x86_64
|
||||||
# elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
|
elif [ "$PLATFORM_NAME" = "linux-arm64" ]; then
|
||||||
# ARCH_NAME=arm64
|
ARCH_NAME=arm64
|
||||||
# export ARCH=aarch64
|
export ARCH=aarch64
|
||||||
# else
|
else
|
||||||
# echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
|
echo "Unexpected PLATFORM_NAME "$PLATFORM_NAME""
|
||||||
# exit 1
|
exit 1
|
||||||
# fi
|
fi
|
||||||
|
|
||||||
# export UFLAG="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|*-$ARCH_NAME.AppImage.zsync"
|
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
|
BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh
|
||||||
# shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Upload Ryujinx artifact
|
- name: Upload Ryujinx artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
@ -112,12 +112,12 @@ jobs:
|
||||||
path: publish
|
path: publish
|
||||||
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
if: github.event_name == 'pull_request' && matrix.platform.os != 'macos-13'
|
||||||
|
|
||||||
#- name: Upload Ryujinx (AppImage) artifact
|
- name: Upload Ryujinx (AppImage) artifact
|
||||||
# uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
# if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
if: github.event_name == 'pull_request' && matrix.platform.os == 'ubuntu-latest'
|
||||||
# with:
|
with:
|
||||||
# name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}-AppImage
|
name: ryujinx-${{ matrix.configuration }}-${{ env.RYUJINX_BASE_VERSION }}+${{ steps.git_short_hash.outputs.result }}-${{ matrix.platform.zip_os_name }}-AppImage
|
||||||
# path: publish_appimage
|
path: publish_appimage
|
||||||
|
|
||||||
- name: Upload Ryujinx.Headless.SDL2 artifact
|
- name: Upload Ryujinx.Headless.SDL2 artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
257
.github/workflows/canary.yml
vendored
Normal file
257
.github/workflows/canary.yml
vendored
Normal file
|
@ -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 -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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- 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 }}
|
114
.github/workflows/release.yml
vendored
114
.github/workflows/release.yml
vendored
|
@ -4,7 +4,7 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs: {}
|
inputs: {}
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ release ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '.github/**'
|
- '.github/**'
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
@ -20,7 +20,7 @@ env:
|
||||||
POWERSHELL_TELEMETRY_OPTOUT: 1
|
POWERSHELL_TELEMETRY_OPTOUT: 1
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
DOTNET_CLI_TELEMETRY_OPTOUT: 1
|
||||||
RYUJINX_BASE_VERSION: "1.2"
|
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_OWNER: "GreemDev"
|
||||||
RYUJINX_TARGET_RELEASE_CHANNEL_REPO: "Ryujinx"
|
RYUJINX_TARGET_RELEASE_CHANNEL_REPO: "Ryujinx"
|
||||||
RELEASE: 1
|
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_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_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_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
|
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
@ -101,83 +102,79 @@ jobs:
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: |
|
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 -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_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 -p:IncludeNativeLibrariesForSelfExtract=true
|
||||||
|
|
||||||
- name: Packing Windows builds
|
- name: Packing Windows builds
|
||||||
if: matrix.platform.os == 'windows-latest'
|
if: matrix.platform.os == 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
pushd publish_ava
|
pushd publish
|
||||||
rm publish/libarmeilleure-jitsupport.dylib
|
rm libarmeilleure-jitsupport.dylib
|
||||||
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
|
7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
|
||||||
popd
|
popd
|
||||||
|
|
||||||
pushd publish_sdl2_headless
|
pushd publish_sdl2_headless
|
||||||
rm publish/libarmeilleure-jitsupport.dylib
|
rm libarmeilleure-jitsupport.dylib
|
||||||
7z a ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip publish
|
7z a ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish
|
||||||
popd
|
popd
|
||||||
shell: bash
|
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
|
- name: Packing Linux builds
|
||||||
if: matrix.platform.os == 'ubuntu-latest'
|
if: matrix.platform.os == 'ubuntu-latest'
|
||||||
run: |
|
run: |
|
||||||
pushd publish_ava
|
pushd publish
|
||||||
rm publish/libarmeilleure-jitsupport.dylib
|
chmod +x Ryujinx.sh Ryujinx
|
||||||
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
|
||||||
tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
pushd publish_sdl2_headless
|
pushd publish_sdl2_headless
|
||||||
rm publish/libarmeilleure-jitsupport.dylib
|
chmod +x Ryujinx.sh Ryujinx.Headless.SDL2
|
||||||
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
|
||||||
tar -czvf ../release_output/sdl2-ryujinx-headless-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz publish
|
|
||||||
popd
|
popd
|
||||||
shell: bash
|
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
|
- name: Pushing new release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.version_info.outputs.build_version }}
|
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/*AppImage*"
|
|
||||||
tag: ${{ steps.version_info.outputs.build_version }}
|
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 }}"
|
body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}"
|
||||||
omitBodyDuringUpdate: true
|
omitBodyDuringUpdate: true
|
||||||
|
@ -228,12 +225,13 @@ 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_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_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_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
|
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Publish macOS Ryujinx
|
- name: Publish macOS Ryujinx
|
||||||
run: |
|
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
|
||||||
|
|
||||||
- name: Publish macOS Ryujinx.Headless.SDL2
|
- name: Publish macOS Ryujinx.Headless.SDL2
|
||||||
run: |
|
run: |
|
||||||
|
@ -243,7 +241,7 @@ jobs:
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.version_info.outputs.build_version }}
|
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 }}
|
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 }}"
|
body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}"
|
||||||
omitBodyDuringUpdate: true
|
omitBodyDuringUpdate: true
|
||||||
|
|
23
COMPILING.md
Normal file
23
COMPILING.md
Normal file
|
@ -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.
|
|
@ -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`).
|
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.
|
- 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.
|
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.
|
- Commit messages should be clear statements of action and intent.
|
||||||
6. Build the repository with your changes.
|
6. Build the repository with your changes.
|
||||||
- Make sure that the builds are clean.
|
- 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.
|
- 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.
|
- 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
|
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.
|
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.
|
- The next official build will automatically include your change.
|
||||||
- You can delete the branch you used for making the change.
|
- You can delete the branch you used for making the change.
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<PackageVersion Include="LibHac" Version="0.19.0" />
|
<PackageVersion Include="LibHac" Version="0.19.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.0.1" />
|
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.1.2" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||||
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||||
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
|
||||||
|
@ -33,6 +33,7 @@
|
||||||
<PackageVersion Include="OpenTK.Graphics" Version="4.8.2" />
|
<PackageVersion Include="OpenTK.Graphics" Version="4.8.2" />
|
||||||
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.8.2" />
|
<PackageVersion Include="OpenTK.Audio.OpenAL" Version="4.8.2" />
|
||||||
<PackageVersion Include="OpenTK.Windowing.GraphicsLibraryFramework" Version="4.8.2" />
|
<PackageVersion Include="OpenTK.Windowing.GraphicsLibraryFramework" Version="4.8.2" />
|
||||||
|
<PackageVersion Include="Open.NAT.Core" Version="2.1.0.5" />
|
||||||
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
|
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
|
||||||
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
|
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
|
||||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||||
|
|
56
README.md
56
README.md
|
@ -14,6 +14,15 @@
|
||||||
<img src="https://img.shields.io/github/v/release/GreemDev/Ryujinx"
|
<img src="https://img.shields.io/github/v/release/GreemDev/Ryujinx"
|
||||||
alt="Latest Release">
|
alt="Latest Release">
|
||||||
</a>
|
</a>
|
||||||
|
<br>
|
||||||
|
<a href="https://github.com/GreemDev/Ryujinx/actions/workflows/canary.yml">
|
||||||
|
<img src="https://github.com/GreemDev/Ryujinx/actions/workflows/canary.yml/badge.svg"
|
||||||
|
alt="">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/GreemDev/Ryujinx-Canary/releases/latest">
|
||||||
|
<img src="https://img.shields.io/github/v/release/GreemDev/Ryujinx-Canary?label=canary"
|
||||||
|
alt="Latest Canary Release">
|
||||||
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -47,55 +56,28 @@
|
||||||
<img src="https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/docs/shell.png">
|
<img src="https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/docs/shell.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 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
|
## Usage
|
||||||
|
|
||||||
To run this emulator, your PC must be equipped with at least 8GiB of RAM;
|
To run this emulator, your PC must be equipped with at least 8GiB of RAM;
|
||||||
failing to meet this requirement may result in a poor gameplay experience or unexpected crashes.
|
failing to meet this requirement may result in a poor gameplay experience or unexpected crashes.
|
||||||
|
|
||||||
## Latest release
|
## Latest build
|
||||||
|
|
||||||
Releases are compiled automatically for each commit on the master branch.
|
Stable builds are made every so often onto a separate "release" branch that then gets put into the releases you know and love.
|
||||||
While we strive to ensure optimal stability and performance prior to pushing an update, our automated builds **may be unstable or completely broken**.
|
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
|
## Documentation
|
||||||
|
|
||||||
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
|
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
Building the project is for advanced users.
|
|
||||||
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
|
## Features
|
||||||
|
|
||||||
- **Audio**
|
- **Audio**
|
||||||
|
|
17
Ryujinx.sln
17
Ryujinx.sln
|
@ -29,14 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Nvdec", "s
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio", "src\Ryujinx.Audio\Ryujinx.Audio.csproj", "{806ACF6D-90B0-45D0-A1AC-5F220F3B3985}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio", "src\Ryujinx.Audio\Ryujinx.Audio.csproj", "{806ACF6D-90B0-45D0-A1AC-5F220F3B3985}"
|
||||||
EndProject
|
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}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Memory", "src\Ryujinx.Memory\Ryujinx.Memory.csproj", "{A5E6C691-9E22-4263-8F40-42F002CE66BE}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Tests.Memory", "src\Ryujinx.Tests.Memory\Ryujinx.Tests.Memory.csproj", "{D1CC5322-7325-4F6B-9625-194B30BE1296}"
|
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
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
|
@ -707,6 +707,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01010300",
|
||||||
|
@ -3526,6 +3542,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01400000",
|
||||||
|
@ -4160,6 +4192,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -5848,6 +5896,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -6126,6 +6190,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -8341,6 +8421,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -9020,6 +9116,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000100",
|
||||||
|
@ -9496,6 +9608,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01010000",
|
||||||
|
@ -9833,6 +9961,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -14667,6 +14811,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01030000",
|
||||||
|
@ -16119,6 +16279,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01050000",
|
||||||
|
@ -16717,6 +16893,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01070000",
|
||||||
|
@ -19745,6 +19937,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01080000",
|
||||||
|
@ -20503,6 +20711,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01010000",
|
||||||
|
@ -21805,6 +22029,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -22340,6 +22580,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01020100",
|
||||||
|
@ -22990,6 +23246,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -23440,6 +23712,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -24660,6 +24948,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01410000",
|
||||||
|
@ -24954,6 +25258,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01060000",
|
||||||
|
@ -25286,6 +25606,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -29114,6 +29450,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01010000",
|
||||||
|
@ -32512,6 +32864,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -32928,6 +33296,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000100",
|
||||||
|
@ -34800,6 +35184,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01000000",
|
||||||
|
@ -37569,6 +37969,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01010100",
|
||||||
|
@ -41293,6 +41709,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01020100",
|
||||||
|
@ -45153,6 +45585,22 @@
|
||||||
"0100F2C0115B6000"
|
"0100F2C0115B6000"
|
||||||
],
|
],
|
||||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
"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",
|
"head": "01010000",
|
||||||
|
@ -47896,5 +48344,5 @@
|
||||||
"type": "Figure"
|
"type": "Figure"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lastUpdated": "2024-10-01T00:00:25.035619"
|
"lastUpdated": "2024-11-17T15:28:47.035619"
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,5 +46,5 @@ then
|
||||||
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$APP_BUNDLE_DIRECTORY"
|
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$APP_BUNDLE_DIRECTORY"
|
||||||
else
|
else
|
||||||
echo "Usign codesign for ad-hoc signing"
|
echo "Usign codesign for ad-hoc signing"
|
||||||
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$APP_BUNDLE_DIRECTORY"
|
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$APP_BUNDLE_DIRECTORY"
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -99,7 +99,7 @@ then
|
||||||
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$UNIVERSAL_APP_BUNDLE"
|
rcodesign sign --entitlements-xml-path "$ENTITLEMENTS_FILE_PATH" "$UNIVERSAL_APP_BUNDLE"
|
||||||
else
|
else
|
||||||
echo "Using codesign for ad-hoc signing"
|
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
|
fi
|
||||||
|
|
||||||
echo "Creating archive"
|
echo "Creating archive"
|
||||||
|
@ -111,4 +111,4 @@ rm "$RELEASE_TAR_FILE_NAME"
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
echo "Done"
|
echo "Done"
|
||||||
|
|
|
@ -95,7 +95,7 @@ else
|
||||||
echo "Using codesign for ad-hoc signing"
|
echo "Using codesign for ad-hoc signing"
|
||||||
for FILE in "$UNIVERSAL_OUTPUT"/*; do
|
for FILE in "$UNIVERSAL_OUTPUT"/*; do
|
||||||
if [[ $(file "$FILE") == *"Mach-O"* ]]; then
|
if [[ $(file "$FILE") == *"Mach-O"* ]]; then
|
||||||
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f --deep -s - "$FILE"
|
codesign --entitlements "$ENTITLEMENTS_FILE_PATH" -f -s - "$FILE"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
@ -108,4 +108,4 @@ gzip -9 < "$RELEASE_TAR_FILE_NAME" > "$RELEASE_TAR_FILE_NAME.gz"
|
||||||
rm "$RELEASE_TAR_FILE_NAME"
|
rm "$RELEASE_TAR_FILE_NAME"
|
||||||
popd
|
popd
|
||||||
|
|
||||||
echo "Done"
|
echo "Done"
|
||||||
|
|
|
@ -9,7 +9,7 @@ To merge pull requests, you must have write permissions in the repository.
|
||||||
## Quick Code Review Rules
|
## 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.
|
* 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.
|
* 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.
|
* 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.
|
* Rebase your changes when required or directly requested. Changes should always be commited on top of the upstream branch, not the other way around.
|
||||||
|
|
|
@ -77,7 +77,7 @@ namespace ARMeilleure.Translation
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int pBlkIndex = 0; pBlkIndex < block.Predecessors.Count; pBlkIndex++)
|
for (int pBlkIndex = 0; pBlkIndex < block.Predecessors.Count; pBlkIndex++)
|
||||||
{
|
{
|
||||||
BasicBlock current = block.Predecessors[pBlkIndex];
|
BasicBlock current = block.Predecessors[pBlkIndex];
|
||||||
|
|
|
@ -13,6 +13,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime;
|
using System.Runtime;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
@ -848,18 +849,15 @@ namespace ARMeilleure.Translation.PTC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Thread> threads = new();
|
|
||||||
|
|
||||||
for (int i = 0; i < degreeOfParallelism; i++)
|
List<Thread> threads = Enumerable.Range(0, degreeOfParallelism)
|
||||||
{
|
.Select(idx =>
|
||||||
Thread thread = new(TranslateFuncs)
|
new Thread(TranslateFuncs)
|
||||||
{
|
{
|
||||||
IsBackground = true,
|
IsBackground = true,
|
||||||
Name = "Ptc.TranslateThread." + i
|
Name = "Ptc.TranslateThread." + idx
|
||||||
};
|
}
|
||||||
|
).ToList();
|
||||||
threads.Add(thread);
|
|
||||||
}
|
|
||||||
|
|
||||||
Stopwatch sw = Stopwatch.StartNew();
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer
|
||||||
public enum MultiplayerMode
|
public enum MultiplayerMode
|
||||||
{
|
{
|
||||||
Disabled,
|
Disabled,
|
||||||
|
LdnRyu,
|
||||||
LdnMitm,
|
LdnMitm,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,5 +72,6 @@ namespace Ryujinx.Common.Logging
|
||||||
TamperMachine,
|
TamperMachine,
|
||||||
UI,
|
UI,
|
||||||
Vic,
|
Vic,
|
||||||
|
XCIFileTrimmer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,10 +30,10 @@ namespace Ryujinx.Common.Logging.Targets
|
||||||
string ILogTarget.Name { get => _target.Name; }
|
string ILogTarget.Name { get => _target.Name; }
|
||||||
|
|
||||||
public AsyncLogTargetWrapper(ILogTarget target)
|
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;
|
_target = target;
|
||||||
_messageQueue = new BlockingCollection<LogEventArgs>(queueLimit);
|
_messageQueue = new BlockingCollection<LogEventArgs>(queueLimit);
|
||||||
|
|
|
@ -47,7 +47,7 @@ namespace Ryujinx.Common.Logging.Targets
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up old logs, should only keep 3
|
// 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++)
|
for (int i = 0; i < files.Length - 2; i++)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
30
src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs
Normal file
30
src/Ryujinx.Common/Logging/XCIFileTrimmerLog.cs
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -803,18 +803,6 @@ namespace Ryujinx.Common.Memory
|
||||||
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
|
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Array256<T> : IArray<T> where T : unmanaged
|
|
||||||
{
|
|
||||||
T _e0;
|
|
||||||
Array128<T> _other;
|
|
||||||
Array127<T> _other2;
|
|
||||||
public readonly int Length => 256;
|
|
||||||
public ref T this[int index] => ref AsSpan()[index];
|
|
||||||
|
|
||||||
[Pure]
|
|
||||||
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Array140<T> : IArray<T> where T : unmanaged
|
public struct Array140<T> : IArray<T> where T : unmanaged
|
||||||
{
|
{
|
||||||
T _e0;
|
T _e0;
|
||||||
|
@ -828,6 +816,18 @@ namespace Ryujinx.Common.Memory
|
||||||
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
|
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Array256<T> : IArray<T> where T : unmanaged
|
||||||
|
{
|
||||||
|
T _e0;
|
||||||
|
Array128<T> _other;
|
||||||
|
Array127<T> _other2;
|
||||||
|
public readonly int Length => 256;
|
||||||
|
public ref T this[int index] => ref AsSpan()[index];
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
|
||||||
|
}
|
||||||
|
|
||||||
public struct Array384<T> : IArray<T> where T : unmanaged
|
public struct Array384<T> : IArray<T> where T : unmanaged
|
||||||
{
|
{
|
||||||
T _e0;
|
T _e0;
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace Ryujinx.Common
|
namespace Ryujinx.Common
|
||||||
{
|
{
|
||||||
public class ReactiveObject<T>
|
public class ReactiveObject<T>
|
||||||
{
|
{
|
||||||
private readonly ReaderWriterLockSlim _readerWriterLock = new();
|
private readonly ReaderWriterLockSlim _rwLock = new();
|
||||||
private bool _isInitialized;
|
private bool _isInitialized;
|
||||||
private T _value;
|
private T _value;
|
||||||
|
|
||||||
|
@ -15,15 +17,15 @@ namespace Ryujinx.Common
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
_readerWriterLock.EnterReadLock();
|
_rwLock.EnterReadLock();
|
||||||
T value = _value;
|
T value = _value;
|
||||||
_readerWriterLock.ExitReadLock();
|
_rwLock.ExitReadLock();
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_readerWriterLock.EnterWriteLock();
|
_rwLock.EnterWriteLock();
|
||||||
|
|
||||||
T oldValue = _value;
|
T oldValue = _value;
|
||||||
|
|
||||||
|
@ -32,7 +34,7 @@ namespace Ryujinx.Common
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
_value = value;
|
_value = value;
|
||||||
|
|
||||||
_readerWriterLock.ExitWriteLock();
|
_rwLock.ExitWriteLock();
|
||||||
|
|
||||||
if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value))
|
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<T> obj) => obj.Value;
|
public static implicit operator T(ReactiveObject<T> obj) => obj.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ReactiveObjectHelper
|
public static class ReactiveObjectHelper
|
||||||
{
|
{
|
||||||
|
public static void LogValueChange<T>(LogClass logClass, ReactiveEventArgs<T> 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<bool> rBoolean) => rBoolean.Value = !rBoolean.Value;
|
public static void Toggle(this ReactiveObject<bool> rBoolean) => rBoolean.Value = !rBoolean.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,9 @@ namespace Ryujinx.Common
|
||||||
// DO NOT EDIT, filled by CI
|
// DO NOT EDIT, filled by CI
|
||||||
public static class ReleaseInformation
|
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%%";
|
private const string BuildVersion = "%%RYUJINX_BUILD_VERSION%%";
|
||||||
public const string BuildGitHash = "%%RYUJINX_BUILD_GIT_HASH%%";
|
public const string BuildGitHash = "%%RYUJINX_BUILD_GIT_HASH%%";
|
||||||
|
@ -13,6 +15,7 @@ namespace Ryujinx.Common
|
||||||
private const string ConfigFileName = "%%RYUJINX_CONFIG_FILE_NAME%%";
|
private const string ConfigFileName = "%%RYUJINX_CONFIG_FILE_NAME%%";
|
||||||
|
|
||||||
public const string ReleaseChannelOwner = "%%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER%%";
|
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 const string ReleaseChannelRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_REPO%%";
|
||||||
|
|
||||||
public static string ConfigName => !ConfigFileName.StartsWith("%%") ? ConfigFileName : "Config.json";
|
public static string ConfigName => !ConfigFileName.StartsWith("%%") ? ConfigFileName : "Config.json";
|
||||||
|
@ -21,10 +24,15 @@ namespace Ryujinx.Common
|
||||||
!BuildGitHash.StartsWith("%%") &&
|
!BuildGitHash.StartsWith("%%") &&
|
||||||
!ReleaseChannelName.StartsWith("%%") &&
|
!ReleaseChannelName.StartsWith("%%") &&
|
||||||
!ReleaseChannelOwner.StartsWith("%%") &&
|
!ReleaseChannelOwner.StartsWith("%%") &&
|
||||||
|
!ReleaseChannelSourceRepo.StartsWith("%%") &&
|
||||||
!ReleaseChannelRepo.StartsWith("%%") &&
|
!ReleaseChannelRepo.StartsWith("%%") &&
|
||||||
!ConfigFileName.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<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
public static string Version => IsValid ? BuildVersion : Assembly.GetEntryAssembly()!.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.Common.Utilities
|
namespace Ryujinx.Common.Utilities
|
||||||
{
|
{
|
||||||
|
@ -65,6 +66,11 @@ namespace Ryujinx.Common.Utilities
|
||||||
return (targetProperties, targetAddressInfo);
|
return (targetProperties, targetAddressInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool SupportsDynamicDns()
|
||||||
|
{
|
||||||
|
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||||
|
}
|
||||||
|
|
||||||
public static uint ConvertIpv4Address(IPAddress ipAddress)
|
public static uint ConvertIpv4Address(IPAddress ipAddress)
|
||||||
{
|
{
|
||||||
return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes());
|
return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes());
|
||||||
|
|
524
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
524
src/Ryujinx.Common/Utilities/XCIFileTrimmer.cs
Normal file
|
@ -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";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cartridge Sizes (ByteIdentifier, SizeInGB)
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Dictionary<byte, long> _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<byte>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL
|
||||||
IPipeline Pipeline { get; }
|
IPipeline Pipeline { get; }
|
||||||
|
|
||||||
IWindow Window { get; }
|
IWindow Window { get; }
|
||||||
|
|
||||||
uint ProgramCount { get; }
|
uint ProgramCount { get; }
|
||||||
|
|
||||||
void BackgroundContextAction(Action action, bool alwaysBackground = false);
|
void BackgroundContextAction(Action action, bool alwaysBackground = false);
|
||||||
|
|
|
@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL
|
||||||
public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
|
public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
|
||||||
{
|
{
|
||||||
ProgramCount++;
|
ProgramCount++;
|
||||||
|
|
||||||
return new Program(shaders, info.FragmentOutputMap);
|
return new Program(shaders, info.FragmentOutputMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,8 +55,10 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
if (_handle != BufferHandle.Null)
|
if (_handle != BufferHandle.Null)
|
||||||
{
|
{
|
||||||
// May need to restride the vertex buffer.
|
// 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);
|
autoBuffer = gd.BufferManager.GetAlignedVertexBuffer(cbs, _handle, _offset, _size, _stride, alignment);
|
||||||
|
|
||||||
|
|
|
@ -549,7 +549,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||||
public IProgram CreateProgram(ShaderSource[] sources, ShaderInfo info)
|
public IProgram CreateProgram(ShaderSource[] sources, ShaderInfo info)
|
||||||
{
|
{
|
||||||
ProgramCount++;
|
ProgramCount++;
|
||||||
|
|
||||||
bool isCompute = sources.Length == 1 && sources[0].Stage == ShaderStage.Compute;
|
bool isCompute = sources.Length == 1 && sources[0].Stage == ShaderStage.Compute;
|
||||||
|
|
||||||
if (info.State.HasValue || isCompute)
|
if (info.State.HasValue || isCompute)
|
||||||
|
|
|
@ -13,6 +13,7 @@ namespace Ryujinx.HLE.Generators
|
||||||
var syntaxReceiver = (ServiceSyntaxReceiver)context.SyntaxReceiver;
|
var syntaxReceiver = (ServiceSyntaxReceiver)context.SyntaxReceiver;
|
||||||
CodeGenerator generator = new CodeGenerator();
|
CodeGenerator generator = new CodeGenerator();
|
||||||
|
|
||||||
|
generator.AppendLine("#nullable enable");
|
||||||
generator.AppendLine("using System;");
|
generator.AppendLine("using System;");
|
||||||
generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm");
|
generator.EnterScope($"namespace Ryujinx.HLE.HOS.Services.Sm");
|
||||||
generator.EnterScope($"partial class IUserInterface");
|
generator.EnterScope($"partial class IUserInterface");
|
||||||
|
@ -58,6 +59,7 @@ namespace Ryujinx.HLE.Generators
|
||||||
|
|
||||||
generator.LeaveScope();
|
generator.LeaveScope();
|
||||||
generator.LeaveScope();
|
generator.LeaveScope();
|
||||||
|
generator.AppendLine("#nullable disable");
|
||||||
context.AddSource($"IUserInterface.g.cs", generator.ToString());
|
context.AddSource($"IUserInterface.g.cs", generator.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,21 @@ namespace Ryujinx.HLE
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MultiplayerMode MultiplayerMode { internal get; set; }
|
public MultiplayerMode MultiplayerMode { internal get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disable P2P mode
|
||||||
|
/// </summary>
|
||||||
|
public bool MultiplayerDisableP2p { internal get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiplayer Passphrase
|
||||||
|
/// </summary>
|
||||||
|
public string MultiplayerLdnPassphrase { internal get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LDN Server
|
||||||
|
/// </summary>
|
||||||
|
public string MultiplayerLdnServer { internal get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An action called when HLE force a refresh of output after docked mode changed.
|
/// An action called when HLE force a refresh of output after docked mode changed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -194,7 +209,10 @@ namespace Ryujinx.HLE
|
||||||
float audioVolume,
|
float audioVolume,
|
||||||
bool useHypervisor,
|
bool useHypervisor,
|
||||||
string multiplayerLanInterfaceId,
|
string multiplayerLanInterfaceId,
|
||||||
MultiplayerMode multiplayerMode)
|
MultiplayerMode multiplayerMode,
|
||||||
|
bool multiplayerDisableP2p,
|
||||||
|
string multiplayerLdnPassphrase,
|
||||||
|
string multiplayerLdnServer)
|
||||||
{
|
{
|
||||||
VirtualFileSystem = virtualFileSystem;
|
VirtualFileSystem = virtualFileSystem;
|
||||||
LibHacHorizonManager = libHacHorizonManager;
|
LibHacHorizonManager = libHacHorizonManager;
|
||||||
|
@ -222,6 +240,9 @@ namespace Ryujinx.HLE
|
||||||
UseHypervisor = useHypervisor;
|
UseHypervisor = useHypervisor;
|
||||||
MultiplayerLanInterfaceId = multiplayerLanInterfaceId;
|
MultiplayerLanInterfaceId = multiplayerLanInterfaceId;
|
||||||
MultiplayerMode = multiplayerMode;
|
MultiplayerMode = multiplayerMode;
|
||||||
|
MultiplayerDisableP2p = multiplayerDisableP2p;
|
||||||
|
MultiplayerLdnPassphrase = multiplayerLdnPassphrase;
|
||||||
|
MultiplayerLdnServer = multiplayerLdnServer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.HOS.Applets.Browser;
|
using Ryujinx.HLE.HOS.Applets.Browser;
|
||||||
|
using Ryujinx.HLE.HOS.Applets.Dummy;
|
||||||
using Ryujinx.HLE.HOS.Applets.Error;
|
using Ryujinx.HLE.HOS.Applets.Error;
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
|
||||||
using System;
|
using System;
|
||||||
|
@ -26,9 +28,13 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
return new BrowserApplet(system);
|
return new BrowserApplet(system);
|
||||||
case AppletId.LibAppletOff:
|
case AppletId.LibAppletOff:
|
||||||
return new BrowserApplet(system);
|
return new BrowserApplet(system);
|
||||||
|
case AppletId.MiiEdit:
|
||||||
|
Logger.Warning?.Print(LogClass.Application, $"Please use the MiiEdit inside File/Open Applet");
|
||||||
|
return new DummyApplet(system);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new NotImplementedException($"{applet} applet is not implemented.");
|
Logger.Warning?.Print(LogClass.Application, $"Applet {applet} not implemented!");
|
||||||
|
return new DummyApplet(system);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
43
src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs
Normal file
43
src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs
Normal file
|
@ -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<T>(byte[] data) where T : struct
|
||||||
|
{
|
||||||
|
return MemoryMarshal.Read<T>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2463,7 +2463,7 @@ namespace Ryujinx.HLE.HOS.Diagnostics.Demangler
|
||||||
return ParseIntegerLiteral("unsigned short");
|
return ParseIntegerLiteral("unsigned short");
|
||||||
case 'i':
|
case 'i':
|
||||||
_position++;
|
_position++;
|
||||||
return ParseIntegerLiteral("");
|
return ParseIntegerLiteral(string.Empty);
|
||||||
case 'j':
|
case 'j':
|
||||||
_position++;
|
_position++;
|
||||||
return ParseIntegerLiteral("u");
|
return ParseIntegerLiteral("u");
|
||||||
|
|
|
@ -116,18 +116,13 @@ namespace Ryujinx.HLE.HOS
|
||||||
private readonly Dictionary<ulong, ModCache> _appMods; // key is ApplicationId
|
private readonly Dictionary<ulong, ModCache> _appMods; // key is ApplicationId
|
||||||
private PatchCache _patches;
|
private PatchCache _patches;
|
||||||
|
|
||||||
private static readonly EnumerationOptions _dirEnumOptions;
|
private static readonly EnumerationOptions _dirEnumOptions = new()
|
||||||
|
|
||||||
static ModLoader()
|
|
||||||
{
|
{
|
||||||
_dirEnumOptions = new EnumerationOptions
|
MatchCasing = MatchCasing.CaseInsensitive,
|
||||||
{
|
MatchType = MatchType.Simple,
|
||||||
MatchCasing = MatchCasing.CaseInsensitive,
|
RecurseSubdirectories = false,
|
||||||
MatchType = MatchType.Simple,
|
ReturnSpecialDirectories = false,
|
||||||
RecurseSubdirectories = false,
|
};
|
||||||
ReturnSpecialDirectories = false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public ModLoader()
|
public ModLoader()
|
||||||
{
|
{
|
||||||
|
@ -169,7 +164,7 @@ namespace Ryujinx.HLE.HOS
|
||||||
foreach (var modDir in dir.EnumerateDirectories())
|
foreach (var modDir in dir.EnumerateDirectories())
|
||||||
{
|
{
|
||||||
types.Clear();
|
types.Clear();
|
||||||
Mod<DirectoryInfo> mod = new("", null, true);
|
Mod<DirectoryInfo> mod = new(string.Empty, null, true);
|
||||||
|
|
||||||
if (StrEquals(RomfsDir, modDir.Name))
|
if (StrEquals(RomfsDir, modDir.Name))
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService;
|
using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
{
|
{
|
||||||
|
@ -25,5 +26,14 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||||
|
|
||||||
return ResultCode.Success;
|
return ResultCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[CommandCmif(350)]
|
||||||
|
// OpenSystemApplicationProxy(u64, pid, handle<copy>) -> object<nn::am::service::IApplicationProxy>
|
||||||
|
public ResultCode OpenSystemApplicationProxy(ServiceCtx context)
|
||||||
|
{
|
||||||
|
MakeObject(context, new IApplicationProxy(context.Request.HandleDesc.PId));
|
||||||
|
|
||||||
|
return ResultCode.Success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 0x20)]
|
[StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 8)]
|
||||||
struct NetworkConfig
|
struct NetworkConfig
|
||||||
{
|
{
|
||||||
public IntentId IntentId;
|
public IntentId IntentId;
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 0x60)]
|
[StructLayout(LayoutKind.Sequential, Size = 0x60, Pack = 8)]
|
||||||
struct ScanFilter
|
struct ScanFilter
|
||||||
{
|
{
|
||||||
public NetworkId NetworkId;
|
public NetworkId NetworkId;
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 0x44)]
|
[StructLayout(LayoutKind.Sequential, Size = 0x44, Pack = 2)]
|
||||||
struct SecurityConfig
|
struct SecurityConfig
|
||||||
{
|
{
|
||||||
public SecurityMode SecurityMode;
|
public SecurityMode SecurityMode;
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 0x20)]
|
[StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)]
|
||||||
struct SecurityParameter
|
struct SecurityParameter
|
||||||
{
|
{
|
||||||
public Array16<byte> Data;
|
public Array16<byte> Data;
|
||||||
|
|
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 0x30)]
|
[StructLayout(LayoutKind.Sequential, Size = 0x30, Pack = 1)]
|
||||||
struct UserConfig
|
struct UserConfig
|
||||||
{
|
{
|
||||||
public Array33<byte> UserName;
|
public Array33<byte> UserName;
|
||||||
|
|
|
@ -15,6 +15,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
public Array8<NodeLatestUpdate> LatestUpdates = new();
|
public Array8<NodeLatestUpdate> LatestUpdates = new();
|
||||||
public bool Connected { get; private set; }
|
public bool Connected { get; private set; }
|
||||||
|
|
||||||
|
public ProxyConfig Config => _parent.NetworkClient.Config;
|
||||||
|
|
||||||
public AccessPoint(IUserLocalCommunicationService parent)
|
public AccessPoint(IUserLocalCommunicationService parent)
|
||||||
{
|
{
|
||||||
_parent = parent;
|
_parent = parent;
|
||||||
|
@ -24,9 +26,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
|
|
||||||
public void Dispose()
|
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)
|
private void NetworkChanged(object sender, NetworkChangeEventArgs e)
|
||||||
|
|
|
@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
{
|
{
|
||||||
interface INetworkClient : IDisposable
|
interface INetworkClient : IDisposable
|
||||||
{
|
{
|
||||||
|
ProxyConfig Config { get; }
|
||||||
bool NeedsRealId { get; }
|
bool NeedsRealId { get; }
|
||||||
|
|
||||||
event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
||||||
|
|
|
@ -9,6 +9,8 @@ using Ryujinx.HLE.HOS.Ipc;
|
||||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm;
|
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.Horizon.Common;
|
||||||
using Ryujinx.Memory;
|
using Ryujinx.Memory;
|
||||||
using System;
|
using System;
|
||||||
|
@ -21,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
{
|
{
|
||||||
class IUserLocalCommunicationService : IpcService, IDisposable
|
class IUserLocalCommunicationService : IpcService, IDisposable
|
||||||
{
|
{
|
||||||
|
public static string DefaultLanPlayHost = "ryuldn.vudjun.com";
|
||||||
|
public static short LanPlayPort = 30456;
|
||||||
|
|
||||||
public INetworkClient NetworkClient { get; private set; }
|
public INetworkClient NetworkClient { get; private set; }
|
||||||
|
|
||||||
private const int NifmRequestID = 90;
|
private const int NifmRequestID = 90;
|
||||||
|
@ -175,19 +180,37 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
|
|
||||||
if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected)
|
if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected)
|
||||||
{
|
{
|
||||||
(_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
|
ProxyConfig config = _state switch
|
||||||
|
|
||||||
if (unicastAddress == null)
|
|
||||||
{
|
{
|
||||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
|
NetworkState.AccessPointCreated => _accessPoint.Config,
|
||||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
|
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
|
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(config.ProxyIp);
|
||||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
|
context.ResponseData.Write(config.ProxySubnetMask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -1066,6 +1089,27 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
|
|
||||||
switch (mode)
|
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:
|
case MultiplayerMode.LdnMitm:
|
||||||
NetworkClient = new LdnMitmClient(context.Device.Configuration);
|
NetworkClient = new LdnMitmClient(context.Device.Configuration);
|
||||||
break;
|
break;
|
||||||
|
@ -1103,7 +1147,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
_accessPoint?.Dispose();
|
_accessPoint?.Dispose();
|
||||||
_accessPoint = null;
|
_accessPoint = null;
|
||||||
|
|
||||||
NetworkClient?.Dispose();
|
NetworkClient?.DisconnectAndStop();
|
||||||
NetworkClient = null;
|
NetworkClient = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||||
using System;
|
using System;
|
||||||
|
@ -6,12 +7,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
{
|
{
|
||||||
class LdnDisabledClient : INetworkClient
|
class LdnDisabledClient : INetworkClient
|
||||||
{
|
{
|
||||||
|
public ProxyConfig Config { get; }
|
||||||
public bool NeedsRealId => true;
|
public bool NeedsRealId => true;
|
||||||
|
|
||||||
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
||||||
|
|
||||||
public NetworkError Connect(ConnectRequest request)
|
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));
|
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
|
||||||
|
|
||||||
return NetworkError.None;
|
return NetworkError.None;
|
||||||
|
@ -19,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
|
|
||||||
public NetworkError ConnectPrivate(ConnectPrivateRequest request)
|
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));
|
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
|
||||||
|
|
||||||
return NetworkError.None;
|
return NetworkError.None;
|
||||||
|
@ -26,6 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
|
|
||||||
public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
|
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));
|
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -33,6 +38,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
|
|
||||||
public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
|
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));
|
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -49,6 +55,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
|
|
||||||
public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
|
public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
|
||||||
{
|
{
|
||||||
|
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to scan for networks, but Multiplayer is disabled!");
|
||||||
return Array.Empty<NetworkInfo>();
|
return Array.Empty<NetworkInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class LdnMitmClient : INetworkClient
|
internal class LdnMitmClient : INetworkClient
|
||||||
{
|
{
|
||||||
|
public ProxyConfig Config { get; }
|
||||||
public bool NeedsRealId => false;
|
public bool NeedsRealId => false;
|
||||||
|
|
||||||
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
|
||||||
|
{
|
||||||
|
interface IProxyClient
|
||||||
|
{
|
||||||
|
bool SendAsync(byte[] buffer);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<NetworkInfo> _availableGames = new List<NetworkInfo>();
|
||||||
|
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<NetworkChangeEventArgs> 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<ushort> 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<NetworkInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ushort> _ephemeralPorts = new List<ushort>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<LdnProxySocket> _sockets = new List<LdnProxySocket>();
|
||||||
|
private readonly Dictionary<ProtocolType, EphemeralPortPool> _ephemeralPorts = new Dictionary<ProtocolType, EphemeralPortPool>();
|
||||||
|
|
||||||
|
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<LdnProxySocket> 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<byte> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
class LdnProxySocket : ISocketImpl
|
||||||
|
{
|
||||||
|
private readonly LdnProxy _proxy;
|
||||||
|
|
||||||
|
private bool _isListening;
|
||||||
|
private readonly List<LdnProxySocket> _listenSockets = new List<LdnProxySocket>();
|
||||||
|
|
||||||
|
private readonly Queue<ProxyConnectRequest> _connectRequests = new Queue<ProxyConnectRequest>();
|
||||||
|
|
||||||
|
private readonly AutoResetEvent _acceptEvent = new AutoResetEvent(false);
|
||||||
|
private readonly int _acceptTimeout = -1;
|
||||||
|
|
||||||
|
private readonly Queue<int> _errors = new Queue<int>();
|
||||||
|
|
||||||
|
private readonly AutoResetEvent _connectEvent = new AutoResetEvent(false);
|
||||||
|
private ProxyConnectResponse _connectResponse;
|
||||||
|
|
||||||
|
private int _receiveTimeout = -1;
|
||||||
|
private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false);
|
||||||
|
private readonly Queue<ProxyDataPacket> _receiveQueue = new Queue<ProxyDataPacket>();
|
||||||
|
|
||||||
|
// 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<SocketOptionName, int> _socketOptions = new Dictionary<SocketOptionName, int>()
|
||||||
|
{
|
||||||
|
{ 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<byte> buffer)
|
||||||
|
{
|
||||||
|
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
|
||||||
|
|
||||||
|
return ReceiveFrom(buffer, SocketFlags.None, ref dummy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Receive(Span<byte> buffer, SocketFlags flags)
|
||||||
|
{
|
||||||
|
EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
|
||||||
|
|
||||||
|
return ReceiveFrom(buffer, flags, ref dummy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Receive(Span<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<P2pProxySession> _players = new List<P2pProxySession>();
|
||||||
|
|
||||||
|
private readonly List<ExternalProxyToken> _waitingTokens = new List<ExternalProxyToken>();
|
||||||
|
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<ushort> 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<P2pProxySession> 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<LdnHeader>();
|
||||||
|
|
||||||
|
private readonly byte[] _buffer = new byte[MaxPacketSize];
|
||||||
|
private int _bufferEnd = 0;
|
||||||
|
|
||||||
|
// Client Packets.
|
||||||
|
public event Action<LdnHeader, InitializeMessage> Initialize;
|
||||||
|
public event Action<LdnHeader, PassphraseMessage> Passphrase;
|
||||||
|
public event Action<LdnHeader, NetworkInfo> Connected;
|
||||||
|
public event Action<LdnHeader, NetworkInfo> SyncNetwork;
|
||||||
|
public event Action<LdnHeader, NetworkInfo> ScanReply;
|
||||||
|
public event Action<LdnHeader> ScanReplyEnd;
|
||||||
|
public event Action<LdnHeader, DisconnectMessage> Disconnected;
|
||||||
|
|
||||||
|
// External Proxy Packets.
|
||||||
|
public event Action<LdnHeader, ExternalProxyConfig> ExternalProxy;
|
||||||
|
public event Action<LdnHeader, ExternalProxyConnectionState> ExternalProxyState;
|
||||||
|
public event Action<LdnHeader, ExternalProxyToken> ExternalProxyToken;
|
||||||
|
|
||||||
|
// Server Packets.
|
||||||
|
public event Action<LdnHeader, CreateAccessPointRequest, byte[]> CreateAccessPoint;
|
||||||
|
public event Action<LdnHeader, CreateAccessPointPrivateRequest, byte[]> CreateAccessPointPrivate;
|
||||||
|
public event Action<LdnHeader, RejectRequest> Reject;
|
||||||
|
public event Action<LdnHeader> RejectReply;
|
||||||
|
public event Action<LdnHeader, SetAcceptPolicyRequest> SetAcceptPolicy;
|
||||||
|
public event Action<LdnHeader, byte[]> SetAdvertiseData;
|
||||||
|
public event Action<LdnHeader, ConnectRequest> Connect;
|
||||||
|
public event Action<LdnHeader, ConnectPrivateRequest> ConnectPrivate;
|
||||||
|
public event Action<LdnHeader, ScanFilter> Scan;
|
||||||
|
|
||||||
|
// Proxy Packets.
|
||||||
|
public event Action<LdnHeader, ProxyConfig> ProxyConfig;
|
||||||
|
public event Action<LdnHeader, ProxyConnectRequest> ProxyConnect;
|
||||||
|
public event Action<LdnHeader, ProxyConnectResponse> ProxyConnectReply;
|
||||||
|
public event Action<LdnHeader, ProxyDataHeader, byte[]> ProxyData;
|
||||||
|
public event Action<LdnHeader, ProxyDisconnectMessage> ProxyDisconnect;
|
||||||
|
|
||||||
|
// Lifecycle Packets.
|
||||||
|
public event Action<LdnHeader, NetworkErrorMessage> NetworkError;
|
||||||
|
public event Action<LdnHeader, PingMessage> 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<byte, LdnHeader>(_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<T>(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<T>(data), remainder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DecodeAndHandle(LdnHeader header, byte[] data)
|
||||||
|
{
|
||||||
|
switch ((PacketId)header.Type)
|
||||||
|
{
|
||||||
|
// Client Packets.
|
||||||
|
case PacketId.Initialize:
|
||||||
|
{
|
||||||
|
Initialize?.Invoke(header, MemoryMarshal.Read<InitializeMessage>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.Passphrase:
|
||||||
|
{
|
||||||
|
Passphrase?.Invoke(header, MemoryMarshal.Read<PassphraseMessage>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.Connected:
|
||||||
|
{
|
||||||
|
Connected?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.SyncNetwork:
|
||||||
|
{
|
||||||
|
SyncNetwork?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.ScanReply:
|
||||||
|
{
|
||||||
|
ScanReply?.Invoke(header, MemoryMarshal.Read<NetworkInfo>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PacketId.ScanReplyEnd:
|
||||||
|
{
|
||||||
|
ScanReplyEnd?.Invoke(header);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.Disconnect:
|
||||||
|
{
|
||||||
|
Disconnected?.Invoke(header, MemoryMarshal.Read<DisconnectMessage>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// External Proxy Packets.
|
||||||
|
case PacketId.ExternalProxy:
|
||||||
|
{
|
||||||
|
ExternalProxy?.Invoke(header, MemoryMarshal.Read<ExternalProxyConfig>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.ExternalProxyState:
|
||||||
|
{
|
||||||
|
ExternalProxyState?.Invoke(header, MemoryMarshal.Read<ExternalProxyConnectionState>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.ExternalProxyToken:
|
||||||
|
{
|
||||||
|
ExternalProxyToken?.Invoke(header, MemoryMarshal.Read<ExternalProxyToken>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server Packets.
|
||||||
|
case PacketId.CreateAccessPoint:
|
||||||
|
{
|
||||||
|
(CreateAccessPointRequest packet, byte[] extraData) = ParseWithData<CreateAccessPointRequest>(data);
|
||||||
|
CreateAccessPoint?.Invoke(header, packet, extraData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.CreateAccessPointPrivate:
|
||||||
|
{
|
||||||
|
(CreateAccessPointPrivateRequest packet, byte[] extraData) = ParseWithData<CreateAccessPointPrivateRequest>(data);
|
||||||
|
CreateAccessPointPrivate?.Invoke(header, packet, extraData);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.Reject:
|
||||||
|
{
|
||||||
|
Reject?.Invoke(header, MemoryMarshal.Read<RejectRequest>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.RejectReply:
|
||||||
|
{
|
||||||
|
RejectReply?.Invoke(header);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.SetAcceptPolicy:
|
||||||
|
{
|
||||||
|
SetAcceptPolicy?.Invoke(header, MemoryMarshal.Read<SetAcceptPolicyRequest>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.SetAdvertiseData:
|
||||||
|
{
|
||||||
|
SetAdvertiseData?.Invoke(header, data);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.Connect:
|
||||||
|
{
|
||||||
|
Connect?.Invoke(header, MemoryMarshal.Read<ConnectRequest>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.ConnectPrivate:
|
||||||
|
{
|
||||||
|
ConnectPrivate?.Invoke(header, MemoryMarshal.Read<ConnectPrivateRequest>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.Scan:
|
||||||
|
{
|
||||||
|
Scan?.Invoke(header, MemoryMarshal.Read<ScanFilter>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proxy Packets
|
||||||
|
case PacketId.ProxyConfig:
|
||||||
|
{
|
||||||
|
ProxyConfig?.Invoke(header, MemoryMarshal.Read<ProxyConfig>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.ProxyConnect:
|
||||||
|
{
|
||||||
|
ProxyConnect?.Invoke(header, MemoryMarshal.Read<ProxyConnectRequest>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.ProxyConnectReply:
|
||||||
|
{
|
||||||
|
ProxyConnectReply?.Invoke(header, MemoryMarshal.Read<ProxyConnectResponse>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.ProxyData:
|
||||||
|
{
|
||||||
|
(ProxyDataHeader packet, byte[] extraData) = ParseWithData<ProxyDataHeader>(data);
|
||||||
|
|
||||||
|
ProxyData?.Invoke(header, packet, extraData);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.ProxyDisconnect:
|
||||||
|
{
|
||||||
|
ProxyDisconnect?.Invoke(header, MemoryMarshal.Read<ProxyDisconnectMessage>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle Packets.
|
||||||
|
case PacketId.Ping:
|
||||||
|
{
|
||||||
|
Ping?.Invoke(header, MemoryMarshal.Read<PingMessage>(data));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PacketId.NetworkError:
|
||||||
|
{
|
||||||
|
NetworkError?.Invoke(header, MemoryMarshal.Read<NetworkErrorMessage>(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<LdnHeader, byte>(ref header).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Encode(PacketId type, byte[] data)
|
||||||
|
{
|
||||||
|
LdnHeader header = GetHeader(type, data.Length);
|
||||||
|
|
||||||
|
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
|
||||||
|
|
||||||
|
Array.Resize(ref result, result.Length + data.Length);
|
||||||
|
Array.Copy(data, 0, result, Marshal.SizeOf<LdnHeader>(), data.Length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Encode<T>(PacketId type, T packet) where T : unmanaged
|
||||||
|
{
|
||||||
|
byte[] packetData = SpanHelpers.AsSpan<T, byte>(ref packet).ToArray();
|
||||||
|
|
||||||
|
LdnHeader header = GetHeader(type, packetData.Length);
|
||||||
|
|
||||||
|
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
|
||||||
|
|
||||||
|
Array.Resize(ref result, result.Length + packetData.Length);
|
||||||
|
Array.Copy(packetData, 0, result, Marshal.SizeOf<LdnHeader>(), packetData.Length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Encode<T>(PacketId type, T packet, byte[] data) where T : unmanaged
|
||||||
|
{
|
||||||
|
byte[] packetData = SpanHelpers.AsSpan<T, byte>(ref packet).ToArray();
|
||||||
|
|
||||||
|
LdnHeader header = GetHeader(type, packetData.Length + data.Length);
|
||||||
|
|
||||||
|
byte[] result = SpanHelpers.AsSpan<LdnHeader, byte>(ref header).ToArray();
|
||||||
|
|
||||||
|
Array.Resize(ref result, result.Length + packetData.Length + data.Length);
|
||||||
|
Array.Copy(packetData, 0, result, Marshal.SizeOf<LdnHeader>(), packetData.Length);
|
||||||
|
Array.Copy(data, 0, result, Marshal.SizeOf<LdnHeader>() + packetData.Length, data.Length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Size = 0x26, Pack = 1)]
|
||||||
|
struct ExternalProxyConfig
|
||||||
|
{
|
||||||
|
public Array16<byte> ProxyIp;
|
||||||
|
public AddressFamily AddressFamily;
|
||||||
|
public ushort ProxyPort;
|
||||||
|
public Array16<byte> Token;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 4)]
|
||||||
|
struct ExternalProxyConnectionState
|
||||||
|
{
|
||||||
|
public uint IpAddress;
|
||||||
|
public bool Connected;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Size = 0x28)]
|
||||||
|
struct ExternalProxyToken
|
||||||
|
{
|
||||||
|
public uint VirtualIp;
|
||||||
|
public Array16<byte> Token;
|
||||||
|
public Array16<byte> PhysicalIp;
|
||||||
|
public AddressFamily AddressFamily;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Ryujinx.Common.Memory;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Size = 0x16)]
|
||||||
|
struct InitializeMessage
|
||||||
|
{
|
||||||
|
// All 0 if we don't have an ID yet.
|
||||||
|
public Array16<byte> Id;
|
||||||
|
|
||||||
|
// All 0 if we don't have a mac yet.
|
||||||
|
public Array6<byte> MacAddress;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<byte> Passphrase;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents data sent over a transport layer.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Size = 0x14)]
|
||||||
|
struct ProxyDataHeader
|
||||||
|
{
|
||||||
|
public ProxyInfo Info;
|
||||||
|
public uint DataLength; // Followed by the data with the specified byte length.
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
|
||||||
|
{
|
||||||
|
class ProxyDataPacket
|
||||||
|
{
|
||||||
|
public ProxyDataHeader Header;
|
||||||
|
public byte[] Data;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Information included in all proxied communication.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 1)]
|
||||||
|
struct ProxyInfo
|
||||||
|
{
|
||||||
|
public uint SourceIpV4;
|
||||||
|
public ushort SourcePort;
|
||||||
|
|
||||||
|
public uint DestIpV4;
|
||||||
|
public ushort DestPort;
|
||||||
|
|
||||||
|
public ProtocolType Protocol;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<byte> 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<byte> PrivateIp;
|
||||||
|
public AddressFamily AddressFamily;
|
||||||
|
public ushort ExternalProxyPort;
|
||||||
|
public ushort InternalProxyPort;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
|
|
||||||
public bool Connected { get; private set; }
|
public bool Connected { get; private set; }
|
||||||
|
|
||||||
|
public ProxyConfig Config => _parent.NetworkClient.Config;
|
||||||
|
|
||||||
public Station(IUserLocalCommunicationService parent)
|
public Station(IUserLocalCommunicationService parent)
|
||||||
{
|
{
|
||||||
_parent = parent;
|
_parent = parent;
|
||||||
|
@ -48,9 +50,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||||
|
|
||||||
public void Dispose()
|
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)
|
private ResultCode NetworkErrorToResult(NetworkError error)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
|
@ -14,5 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
public UserConfig UserConfig;
|
public UserConfig UserConfig;
|
||||||
public NetworkConfig NetworkConfig;
|
public NetworkConfig NetworkConfig;
|
||||||
public AddressList AddressList;
|
public AddressList AddressList;
|
||||||
|
|
||||||
|
public RyuNetworkConfig RyuNetworkConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
|
@ -6,11 +7,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Advertise data is appended separately (remaining data in the buffer).
|
/// Advertise data is appended separately (remaining data in the buffer).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)]
|
[StructLayout(LayoutKind.Sequential, Size = 0xBC, Pack = 1)]
|
||||||
struct CreateAccessPointRequest
|
struct CreateAccessPointRequest
|
||||||
{
|
{
|
||||||
public SecurityConfig SecurityConfig;
|
public SecurityConfig SecurityConfig;
|
||||||
public UserConfig UserConfig;
|
public UserConfig UserConfig;
|
||||||
public NetworkConfig NetworkConfig;
|
public NetworkConfig NetworkConfig;
|
||||||
|
|
||||||
|
public RyuNetworkConfig RyuNetworkConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager
|
||||||
public uint FileVersion { get; set; }
|
public uint FileVersion { get; set; }
|
||||||
public byte[] TagUuid { get; set; }
|
public byte[] TagUuid { get; set; }
|
||||||
public string AmiiboId { get; set; }
|
public string AmiiboId { get; set; }
|
||||||
|
public string NickName { get; set; }
|
||||||
public DateTime FirstWriteDate { get; set; }
|
public DateTime FirstWriteDate { get; set; }
|
||||||
public DateTime LastWriteDate { get; set; }
|
public DateTime LastWriteDate { get; set; }
|
||||||
public ushort WriteCounter { get; set; }
|
public ushort WriteCounter { get; set; }
|
||||||
|
|
|
@ -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);
|
VirtualAmiiboFile amiiboFile = LoadAmiiboFile(amiiboId);
|
||||||
|
string nickname = amiiboFile.NickName ?? "Ryujinx";
|
||||||
UtilityImpl utilityImpl = new(tickSource);
|
UtilityImpl utilityImpl = new(tickSource);
|
||||||
CharInfo charInfo = new();
|
CharInfo charInfo = new();
|
||||||
|
|
||||||
charInfo.SetFromStoreData(StoreData.BuildDefault(utilityImpl, 0));
|
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()
|
RegisterInfo registerInfo = new()
|
||||||
{
|
{
|
||||||
|
@ -85,7 +86,9 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
||||||
Reserved1 = new Array64<byte>(),
|
Reserved1 = new Array64<byte>(),
|
||||||
Reserved2 = new Array58<byte>(),
|
Reserved2 = new Array58<byte>(),
|
||||||
};
|
};
|
||||||
"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;
|
return registerInfo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
|
||||||
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
|
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
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 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 RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;
|
||||||
|
|
||||||
public IPEndPoint LocalEndPoint => Socket.LocalEndPoint 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;
|
Refcount = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ManagedSocket(Socket socket)
|
private ManagedSocket(ISocketImpl socket)
|
||||||
{
|
{
|
||||||
Socket = socket;
|
Socket = socket;
|
||||||
Refcount = 1;
|
Refcount = 1;
|
||||||
|
@ -185,6 +186,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool hasEmittedBlockingWarning = false;
|
||||||
|
|
||||||
public LinuxError Receive(out int receiveSize, Span<byte> buffer, BsdSocketFlags flags)
|
public LinuxError Receive(out int receiveSize, Span<byte> buffer, BsdSocketFlags flags)
|
||||||
{
|
{
|
||||||
LinuxError result;
|
LinuxError result;
|
||||||
|
@ -199,6 +202,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||||
shouldBlockAfterOperation = true;
|
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));
|
receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags));
|
||||||
|
|
||||||
result = LinuxError.SUCCESS;
|
result = LinuxError.SUCCESS;
|
||||||
|
@ -236,6 +245,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||||
shouldBlockAfterOperation = true;
|
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)
|
if (!Socket.IsBound)
|
||||||
{
|
{
|
||||||
receiveSize = -1;
|
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}");
|
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported GetSockOpt Option: {option} Level: {level}");
|
||||||
optionValue.Clear();
|
optionValue.Clear();
|
||||||
|
|
||||||
return LinuxError.SUCCESS;
|
return LinuxError.EOPNOTSUPP;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] tempOptionValue = new byte[optionValue.Length];
|
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}");
|
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported SetSockOpt Option: {option} Level: {level}");
|
||||||
|
|
||||||
return LinuxError.SUCCESS;
|
return LinuxError.EOPNOTSUPP;
|
||||||
}
|
}
|
||||||
|
|
||||||
int value = optionValue.Length >= 4 ? MemoryMarshal.Read<int>(optionValue) : MemoryMarshal.Read<byte>(optionValue);
|
int value = optionValue.Length >= 4 ? MemoryMarshal.Read<int>(optionValue) : MemoryMarshal.Read<byte>(optionValue);
|
||||||
|
@ -493,7 +508,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||||
|
|
||||||
try
|
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)
|
if (receiveSize > 0)
|
||||||
{
|
{
|
||||||
|
@ -531,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||||
|
|
||||||
try
|
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)
|
if (sendSize > 0)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
|
||||||
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
|
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
@ -26,45 +27,46 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||||
|
|
||||||
public LinuxError Poll(List<PollEvent> events, int timeoutMilliseconds, out int updatedCount)
|
public LinuxError Poll(List<PollEvent> events, int timeoutMilliseconds, out int updatedCount)
|
||||||
{
|
{
|
||||||
List<Socket> readEvents = new();
|
List<ISocketImpl> readEvents = new();
|
||||||
List<Socket> writeEvents = new();
|
List<ISocketImpl> writeEvents = new();
|
||||||
List<Socket> errorEvents = new();
|
List<ISocketImpl> errorEvents = new();
|
||||||
|
|
||||||
updatedCount = 0;
|
updatedCount = 0;
|
||||||
|
|
||||||
foreach (PollEvent evnt in events)
|
foreach (PollEvent evnt in events)
|
||||||
{
|
{
|
||||||
ManagedSocket socket = (ManagedSocket)evnt.FileDescriptor;
|
if (evnt.FileDescriptor is ManagedSocket ms)
|
||||||
|
|
||||||
bool isValidEvent = evnt.Data.InputEvents == 0;
|
|
||||||
|
|
||||||
errorEvents.Add(socket.Socket);
|
|
||||||
|
|
||||||
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
|
|
||||||
{
|
{
|
||||||
readEvents.Add(socket.Socket);
|
bool isValidEvent = evnt.Data.InputEvents == 0;
|
||||||
|
|
||||||
isValidEvent = true;
|
errorEvents.Add(ms.Socket);
|
||||||
}
|
|
||||||
|
|
||||||
if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0)
|
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
|
||||||
{
|
{
|
||||||
readEvents.Add(socket.Socket);
|
readEvents.Add(ms.Socket);
|
||||||
|
|
||||||
isValidEvent = true;
|
isValidEvent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0)
|
if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0)
|
||||||
{
|
{
|
||||||
writeEvents.Add(socket.Socket);
|
readEvents.Add(ms.Socket);
|
||||||
|
|
||||||
isValidEvent = true;
|
isValidEvent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidEvent)
|
if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0)
|
||||||
{
|
{
|
||||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}");
|
writeEvents.Add(ms.Socket);
|
||||||
return LinuxError.EINVAL;
|
|
||||||
|
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;
|
int actualTimeoutMicroseconds = timeoutMilliseconds == -1 ? -1 : timeoutMilliseconds * 1000;
|
||||||
|
|
||||||
Socket.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds);
|
SocketHelpers.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds);
|
||||||
}
|
}
|
||||||
catch (SocketException exception)
|
catch (SocketException exception)
|
||||||
{
|
{
|
||||||
|
@ -81,34 +83,37 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||||
|
|
||||||
foreach (PollEvent evnt in events)
|
foreach (PollEvent evnt in events)
|
||||||
{
|
{
|
||||||
Socket socket = ((ManagedSocket)evnt.FileDescriptor).Socket;
|
if (evnt.FileDescriptor is ManagedSocket ms)
|
||||||
|
|
||||||
PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents;
|
|
||||||
|
|
||||||
if (errorEvents.Contains(socket))
|
|
||||||
{
|
{
|
||||||
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 (!socket.Connected || !socket.IsBound)
|
||||||
{
|
{
|
||||||
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
|
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))
|
if (writeEvents.Contains(ms.Socket))
|
||||||
{
|
{
|
||||||
outputEvents |= PollEventTypeMask.Output;
|
outputEvents |= PollEventTypeMask.Output;
|
||||||
}
|
}
|
||||||
|
|
||||||
evnt.Data.OutputEvents = outputEvents;
|
evnt.Data.OutputEvents = outputEvents;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
|
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
|
||||||
|
@ -118,53 +123,55 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||||
|
|
||||||
public LinuxError Select(List<PollEvent> events, int timeout, out int updatedCount)
|
public LinuxError Select(List<PollEvent> events, int timeout, out int updatedCount)
|
||||||
{
|
{
|
||||||
List<Socket> readEvents = new();
|
List<ISocketImpl> readEvents = new();
|
||||||
List<Socket> writeEvents = new();
|
List<ISocketImpl> writeEvents = new();
|
||||||
List<Socket> errorEvents = new();
|
List<ISocketImpl> errorEvents = new();
|
||||||
|
|
||||||
updatedCount = 0;
|
updatedCount = 0;
|
||||||
|
|
||||||
foreach (PollEvent pollEvent in events)
|
foreach (PollEvent pollEvent in events)
|
||||||
{
|
{
|
||||||
ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor;
|
if (pollEvent.FileDescriptor is ManagedSocket ms)
|
||||||
|
|
||||||
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input))
|
|
||||||
{
|
{
|
||||||
readEvents.Add(socket.Socket);
|
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input))
|
||||||
}
|
{
|
||||||
|
readEvents.Add(ms.Socket);
|
||||||
|
}
|
||||||
|
|
||||||
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output))
|
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output))
|
||||||
{
|
{
|
||||||
writeEvents.Add(socket.Socket);
|
writeEvents.Add(ms.Socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error))
|
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error))
|
||||||
{
|
{
|
||||||
errorEvents.Add(socket.Socket);
|
errorEvents.Add(ms.Socket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Socket.Select(readEvents, writeEvents, errorEvents, timeout);
|
SocketHelpers.Select(readEvents, writeEvents, errorEvents, timeout);
|
||||||
|
|
||||||
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
|
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
|
||||||
|
|
||||||
foreach (PollEvent pollEvent in events)
|
foreach (PollEvent pollEvent in events)
|
||||||
{
|
{
|
||||||
ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor;
|
if (pollEvent.FileDescriptor is ManagedSocket ms)
|
||||||
|
|
||||||
if (readEvents.Contains(socket.Socket))
|
|
||||||
{
|
{
|
||||||
pollEvent.Data.OutputEvents |= PollEventTypeMask.Input;
|
if (readEvents.Contains(ms.Socket))
|
||||||
}
|
{
|
||||||
|
pollEvent.Data.OutputEvents |= PollEventTypeMask.Input;
|
||||||
|
}
|
||||||
|
|
||||||
if (writeEvents.Contains(socket.Socket))
|
if (writeEvents.Contains(ms.Socket))
|
||||||
{
|
{
|
||||||
pollEvent.Data.OutputEvents |= PollEventTypeMask.Output;
|
pollEvent.Data.OutputEvents |= PollEventTypeMask.Output;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorEvents.Contains(socket.Socket))
|
if (errorEvents.Contains(ms.Socket))
|
||||||
{
|
{
|
||||||
pollEvent.Data.OutputEvents |= PollEventTypeMask.Error;
|
pollEvent.Data.OutputEvents |= PollEventTypeMask.Error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
178
src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs
Normal file
178
src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs
Normal file
|
@ -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<byte> buffer)
|
||||||
|
{
|
||||||
|
EnsureNetworkInterfaceBound();
|
||||||
|
|
||||||
|
return BaseSocket.Receive(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Receive(Span<byte> buffer, SocketFlags flags)
|
||||||
|
{
|
||||||
|
EnsureNetworkInterfaceBound();
|
||||||
|
|
||||||
|
return BaseSocket.Receive(buffer, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Receive(Span<byte> buffer, SocketFlags flags, out SocketError socketError)
|
||||||
|
{
|
||||||
|
EnsureNetworkInterfaceBound();
|
||||||
|
|
||||||
|
return BaseSocket.Receive(buffer, flags, out socketError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ReceiveFrom(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEP)
|
||||||
|
{
|
||||||
|
EnsureNetworkInterfaceBound();
|
||||||
|
|
||||||
|
return BaseSocket.ReceiveFrom(buffer, flags, ref remoteEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Send(ReadOnlySpan<byte> buffer)
|
||||||
|
{
|
||||||
|
EnsureNetworkInterfaceBound();
|
||||||
|
|
||||||
|
return BaseSocket.Send(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags)
|
||||||
|
{
|
||||||
|
EnsureNetworkInterfaceBound();
|
||||||
|
|
||||||
|
return BaseSocket.Send(buffer, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Send(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError)
|
||||||
|
{
|
||||||
|
EnsureNetworkInterfaceBound();
|
||||||
|
|
||||||
|
return BaseSocket.Send(buffer, flags, out socketError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SendTo(ReadOnlySpan<byte> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs
Normal file
47
src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs
Normal file
|
@ -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<byte> buffer);
|
||||||
|
int Receive(Span<byte> buffer, SocketFlags flags);
|
||||||
|
int Receive(Span<byte> buffer, SocketFlags flags, out SocketError socketError);
|
||||||
|
int ReceiveFrom(Span<byte> buffer, SocketFlags flags, ref EndPoint remoteEP);
|
||||||
|
|
||||||
|
int Send(ReadOnlySpan<byte> buffer);
|
||||||
|
int Send(ReadOnlySpan<byte> buffer, SocketFlags flags);
|
||||||
|
int Send(ReadOnlySpan<byte> buffer, SocketFlags flags, out SocketError socketError);
|
||||||
|
int SendTo(ReadOnlySpan<byte> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ISocketImpl> readEvents, List<ISocketImpl> writeEvents, List<ISocketImpl> 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<ISocketImpl> removeFrom, List<Socket> selectedSockets, Func<LdnProxySocket, bool> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -292,7 +292,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres
|
||||||
{
|
{
|
||||||
string host = MemoryHelper.ReadAsciiString(context.Memory, inputBufferPosition, (int)inputBufferSize);
|
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}");
|
Logger.Info?.Print(LogClass.ServiceSfdnsres, $"Guest network access disabled, DNS Blocked: {host}");
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using Ryujinx.HLE.HOS.Services.Sockets.Bsd;
|
using Ryujinx.HLE.HOS.Services.Sockets.Bsd;
|
||||||
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl;
|
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
|
||||||
using Ryujinx.HLE.HOS.Services.Ssl.Types;
|
using Ryujinx.HLE.HOS.Services.Ssl.Types;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -116,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService
|
||||||
public ResultCode Handshake(string hostName)
|
public ResultCode Handshake(string hostName)
|
||||||
{
|
{
|
||||||
StartSslOperation();
|
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);
|
hostName = RetrieveHostName(hostName);
|
||||||
_stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false);
|
_stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false);
|
||||||
EndSslOperation();
|
EndSslOperation();
|
||||||
|
|
|
@ -85,8 +85,8 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: LibHac npdm currently doesn't support version field.
|
// TODO: LibHac npdm currently doesn't support version field.
|
||||||
string version = ProgramId > 0x0100000000007FFF
|
string version = ProgramId > 0x0100000000007FFF
|
||||||
? DisplayVersion
|
? DisplayVersion
|
||||||
: device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?";
|
: device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?";
|
||||||
|
|
||||||
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]");
|
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]");
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
<PackageReference Include="SkiaSharp" />
|
<PackageReference Include="SkiaSharp" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||||
<PackageReference Include="NetCoreServer" />
|
<PackageReference Include="NetCoreServer" />
|
||||||
|
<PackageReference Include="Open.NAT.Core" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -117,8 +117,9 @@ namespace Ryujinx.Headless.SDL2.OpenGL
|
||||||
GraphicsDebugLevel glLogLevel,
|
GraphicsDebugLevel glLogLevel,
|
||||||
AspectRatio aspectRatio,
|
AspectRatio aspectRatio,
|
||||||
bool enableMouse,
|
bool enableMouse,
|
||||||
HideCursorMode hideCursorMode)
|
HideCursorMode hideCursorMode,
|
||||||
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
|
bool ignoreControllerApplet)
|
||||||
|
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
|
||||||
{
|
{
|
||||||
_glLogLevel = glLogLevel;
|
_glLogLevel = glLogLevel;
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,6 +225,9 @@ namespace Ryujinx.Headless.SDL2
|
||||||
|
|
||||||
[Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")]
|
[Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")]
|
||||||
public bool IgnoreMissingServices { get; set; }
|
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
|
// Values
|
||||||
|
|
||||||
|
|
|
@ -444,8 +444,7 @@ namespace Ryujinx.Headless.SDL2
|
||||||
{
|
{
|
||||||
Logger.AddTarget(new AsyncLogTargetWrapper(
|
Logger.AddTarget(new AsyncLogTargetWrapper(
|
||||||
new FileLogTarget("file", logFile),
|
new FileLogTarget("file", logFile),
|
||||||
1000,
|
1000
|
||||||
AsyncLogTargetOverflowAction.Block
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -506,8 +505,8 @@ namespace Ryujinx.Headless.SDL2
|
||||||
private static WindowBase CreateWindow(Options options)
|
private static WindowBase CreateWindow(Options options)
|
||||||
{
|
{
|
||||||
return options.GraphicsBackend == GraphicsBackend.Vulkan
|
return options.GraphicsBackend == GraphicsBackend.Vulkan
|
||||||
? new VulkanWindow(_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);
|
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IRenderer CreateRenderer(Options options, WindowBase window)
|
private static IRenderer CreateRenderer(Options options, WindowBase window)
|
||||||
|
@ -578,7 +577,10 @@ namespace Ryujinx.Headless.SDL2
|
||||||
options.AudioVolume,
|
options.AudioVolume,
|
||||||
options.UseHypervisor ?? true,
|
options.UseHypervisor ?? true,
|
||||||
options.MultiplayerLanInterfaceId,
|
options.MultiplayerLanInterfaceId,
|
||||||
Common.Configuration.Multiplayer.MultiplayerMode.Disabled);
|
Common.Configuration.Multiplayer.MultiplayerMode.Disabled,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
|
||||||
return new Switch(configuration);
|
return new Switch(configuration);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
|
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$([MSBuild]::IsOSPlatform('OSX'))">
|
||||||
<Exec Command="codesign --entitlements '$(ProjectDir)..\..\distribution\macos\entitlements.xml' -f --deep -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
|
<Exec Command="codesign --entitlements '$(ProjectDir)..\..\distribution\macos\entitlements.xml' -f -s $(SigningCertificate) '$(TargetDir)$(TargetName)'" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -17,8 +17,9 @@ namespace Ryujinx.Headless.SDL2.Vulkan
|
||||||
GraphicsDebugLevel glLogLevel,
|
GraphicsDebugLevel glLogLevel,
|
||||||
AspectRatio aspectRatio,
|
AspectRatio aspectRatio,
|
||||||
bool enableMouse,
|
bool enableMouse,
|
||||||
HideCursorMode hideCursorMode)
|
HideCursorMode hideCursorMode,
|
||||||
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
|
bool ignoreControllerApplet)
|
||||||
|
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
|
||||||
{
|
{
|
||||||
_glLogLevel = glLogLevel;
|
_glLogLevel = glLogLevel;
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,13 +86,15 @@ namespace Ryujinx.Headless.SDL2
|
||||||
|
|
||||||
private readonly AspectRatio _aspectRatio;
|
private readonly AspectRatio _aspectRatio;
|
||||||
private readonly bool _enableMouse;
|
private readonly bool _enableMouse;
|
||||||
|
private readonly bool _ignoreControllerApplet;
|
||||||
|
|
||||||
public WindowBase(
|
public WindowBase(
|
||||||
InputManager inputManager,
|
InputManager inputManager,
|
||||||
GraphicsDebugLevel glLogLevel,
|
GraphicsDebugLevel glLogLevel,
|
||||||
AspectRatio aspectRatio,
|
AspectRatio aspectRatio,
|
||||||
bool enableMouse,
|
bool enableMouse,
|
||||||
HideCursorMode hideCursorMode)
|
HideCursorMode hideCursorMode,
|
||||||
|
bool ignoreControllerApplet)
|
||||||
{
|
{
|
||||||
MouseDriver = new SDL2MouseDriver(hideCursorMode);
|
MouseDriver = new SDL2MouseDriver(hideCursorMode);
|
||||||
_inputManager = inputManager;
|
_inputManager = inputManager;
|
||||||
|
@ -108,6 +110,7 @@ namespace Ryujinx.Headless.SDL2
|
||||||
_gpuDoneEvent = new ManualResetEvent(false);
|
_gpuDoneEvent = new ManualResetEvent(false);
|
||||||
_aspectRatio = aspectRatio;
|
_aspectRatio = aspectRatio;
|
||||||
_enableMouse = enableMouse;
|
_enableMouse = enableMouse;
|
||||||
|
_ignoreControllerApplet = ignoreControllerApplet;
|
||||||
HostUITheme = new HeadlessHostUiTheme();
|
HostUITheme = new HeadlessHostUiTheme();
|
||||||
|
|
||||||
SDL2Driver.Instance.Initialize();
|
SDL2Driver.Instance.Initialize();
|
||||||
|
@ -484,6 +487,8 @@ namespace Ryujinx.Headless.SDL2
|
||||||
|
|
||||||
public bool DisplayMessageDialog(ControllerAppletUIArgs args)
|
public bool DisplayMessageDialog(ControllerAppletUIArgs args)
|
||||||
{
|
{
|
||||||
|
if (_ignoreControllerApplet) return false;
|
||||||
|
|
||||||
string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
|
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"
|
string message = $"Application requests {playerCount} {"player".ToQuantity(args.PlayerCountMin + args.PlayerCountMax, ShowQuantityAs.None)} with:\n\n"
|
||||||
|
|
|
@ -115,7 +115,10 @@ namespace Ryujinx.Input.SDL2
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_gamepadsIds.Insert(joystickDeviceId, id);
|
if (joystickDeviceId <= _gamepadsIds.FindLastIndex(_ => true))
|
||||||
|
_gamepadsIds.Insert(joystickDeviceId, id);
|
||||||
|
else
|
||||||
|
_gamepadsIds.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
OnGamepadConnected?.Invoke(id);
|
OnGamepadConnected?.Invoke(id);
|
||||||
|
|
|
@ -27,6 +27,8 @@ namespace Ryujinx.UI.App.Common
|
||||||
public ulong Id { get; set; }
|
public ulong Id { get; set; }
|
||||||
public string Developer { get; set; } = "Unknown";
|
public string Developer { get; set; } = "Unknown";
|
||||||
public string Version { get; set; } = "0";
|
public string Version { get; set; } = "0";
|
||||||
|
public int PlayerCount { get; set; }
|
||||||
|
public int GameCount { get; set; }
|
||||||
public TimeSpan TimePlayed { get; set; }
|
public TimeSpan TimePlayed { get; set; }
|
||||||
public DateTime? LastPlayed { get; set; }
|
public DateTime? LastPlayed { get; set; }
|
||||||
public string FileExtension { get; set; }
|
public string FileExtension { get; set; }
|
||||||
|
|
|
@ -12,6 +12,7 @@ using LibHac.Tools.Fs;
|
||||||
using LibHac.Tools.FsSystem;
|
using LibHac.Tools.FsSystem;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Configuration.Multiplayer;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
|
@ -27,10 +28,12 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using ContentType = LibHac.Ncm.ContentType;
|
using ContentType = LibHac.Ncm.ContentType;
|
||||||
using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
|
using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
|
||||||
using Path = System.IO.Path;
|
using Path = System.IO.Path;
|
||||||
|
@ -41,8 +44,10 @@ namespace Ryujinx.UI.App.Common
|
||||||
{
|
{
|
||||||
public class ApplicationLibrary
|
public class ApplicationLibrary
|
||||||
{
|
{
|
||||||
|
public static string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
|
||||||
public Language DesiredLanguage { get; set; }
|
public Language DesiredLanguage { get; set; }
|
||||||
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
|
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
|
||||||
|
public event EventHandler<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
|
||||||
|
|
||||||
public readonly IObservableCache<ApplicationData, ulong> Applications;
|
public readonly IObservableCache<ApplicationData, ulong> Applications;
|
||||||
public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates;
|
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 readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc);
|
||||||
|
|
||||||
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||||
|
|
||||||
public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
|
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 ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0) ||
|
||||||
(Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI) ||
|
(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 ".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)
|
(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
|
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
|
||||||
foreach (string applicationPath in applicationPaths)
|
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<LdnGameData> ldnGameDataArray = Array.Empty<LdnGameData>();
|
||||||
|
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<LdnGameData>()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs()
|
||||||
|
{
|
||||||
|
LdnData = Array.Empty<LdnGameData>()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Replace the currently stored DLC state for the game with the provided DLC state.
|
// Replace the currently stored DLC state for the game with the provided DLC state.
|
||||||
public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs)
|
public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs)
|
||||||
{
|
{
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue