mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-07-30 01:29:48 -06:00
Compare commits
31 Commits
Canary-1.2
...
1.2.75
Author | SHA1 | Date | |
---|---|---|---|
fda79efed4 | |||
8444e4dca0 | |||
008d908c5a | |||
722953211d | |||
df5002bdbf | |||
f4b757c584 | |||
25d69079cb | |||
2e1ede5348 | |||
52f42d450f | |||
11416e2167 | |||
e5d076a1b2 | |||
d394dd769a | |||
6de3afc43d | |||
9b90e81817 | |||
1e53a17041 | |||
0c23104792 | |||
1ed2aea029 | |||
34caa03385 | |||
104701e80d | |||
cef88febb2 | |||
5fccfb76b9 | |||
4cb5946be4 | |||
e1dfb48e23 | |||
6d8738c048 | |||
abfcfcaf0f | |||
d404a8b05b | |||
42cbe24bb1 | |||
79ba9d1258 | |||
826ffd4a04 | |||
7369079459 | |||
a506d81989 |
3
.github/workflows/canary.yml
vendored
3
.github/workflows/canary.yml
vendored
@ -22,6 +22,7 @@ env:
|
||||
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
|
||||
|
||||
@ -93,6 +94,7 @@ jobs:
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_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
|
||||
|
||||
@ -228,6 +230,7 @@ jobs:
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_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
|
||||
|
||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -93,6 +93,7 @@ jobs:
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
|
||||
@ -173,7 +174,7 @@ jobs:
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ steps.version_info.outputs.build_version }}
|
||||
artifacts: "release_output/*.tar.gz,release_output/*.zip/*AppImage*"
|
||||
artifacts: "release_output/*.tar.gz,release_output/*.zip,release_output/*AppImage*"
|
||||
tag: ${{ steps.version_info.outputs.build_version }}
|
||||
body: "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.version_info.outputs.prev_build_version }}...${{ steps.version_info.outputs.build_version }}"
|
||||
omitBodyDuringUpdate: true
|
||||
@ -224,6 +225,7 @@ jobs:
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_NAME\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_NAME }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_OWNER }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO\%\%/${{ env.RYUJINX_TARGET_RELEASE_CHANNEL_REPO }}/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
sed -r --in-place 's/\%\%RYUJINX_CONFIG_FILE_NAME\%\%/Config\.json/g;' src/Ryujinx.Common/ReleaseInformation.cs
|
||||
shell: bash
|
||||
|
||||
|
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`).
|
||||
- Branches are useful since they isolate your changes from incoming changes from upstream. They also enable you to create multiple PRs from the same fork.
|
||||
4. Make and commit your changes to your branch.
|
||||
- [Build Instructions](https://github.com/GreemDev/Ryujinx#building) explains how to build and test.
|
||||
- [Build Instructions](https://github.com/GreemDev/Ryujinx/blob/master/COMPILING.md) explains how to build and test.
|
||||
- Commit messages should be clear statements of action and intent.
|
||||
6. Build the repository with your changes.
|
||||
- Make sure that the builds are clean.
|
||||
@ -83,7 +83,7 @@ We use and recommend the following workflow:
|
||||
- State in the description what issue or improvement your change is addressing.
|
||||
- Check if all the Continuous Integration checks are passing. Refer to [Actions](https://github.com/GreemDev/Ryujinx/actions) to check for outstanding errors.
|
||||
8. Wait for feedback or approval of your changes from the core development team
|
||||
- Details about the pull request [review procedure](docs/workflow/ci/pr-guide.md).
|
||||
- Details about the pull request [review procedure](docs/workflow/pr-guide.md).
|
||||
9. When the team members have signed off, and all checks are green, your PR will be merged.
|
||||
- The next official build will automatically include your change.
|
||||
- You can delete the branch you used for making the change.
|
||||
|
@ -33,6 +33,7 @@
|
||||
<PackageVersion Include="OpenTK.Graphics" Version="4.8.2" />
|
||||
<PackageVersion Include="OpenTK.Audio.OpenAL" 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.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||
|
47
README.md
47
README.md
@ -56,55 +56,28 @@
|
||||
<img src="https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/docs/shell.png">
|
||||
</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
|
||||
|
||||
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.
|
||||
|
||||
## Latest release
|
||||
## Latest build
|
||||
|
||||
Releases are compiled automatically for each commit on the master branch.
|
||||
While we strive to ensure optimal stability and performance prior to pushing an update, our automated builds **may be unstable or completely broken**.
|
||||
Stable builds are made every so often onto a separate "release" branch that then gets put into the releases you know and love.
|
||||
These stable builds exist so that the end user can get a more **enjoyable and stable experience**.
|
||||
|
||||
You can find the latest release [here](https://github.com/GreemDev/Ryujinx/releases/latest).
|
||||
You can find the latest stable release [here](https://github.com/GreemDev/Ryujinx/releases/latest).
|
||||
|
||||
Canary builds are compiled automatically for each commit on the master branch.
|
||||
While we strive to ensure optimal stability and performance prior to pushing an update, these builds **may be unstable or completely broken**.
|
||||
These canary builds are only recommended for experienced users.
|
||||
|
||||
You can find the latest canary release [here](https://github.com/GreemDev/Ryujinx-Canary/releases/latest).
|
||||
|
||||
## Documentation
|
||||
|
||||
If you are planning to contribute or just want to learn more about this project please read through our [documentation](docs/README.md).
|
||||
|
||||
## Building
|
||||
|
||||
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
|
||||
|
||||
- **Audio**
|
||||
|
@ -707,6 +707,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Blue Attire",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01010300",
|
||||
@ -3526,6 +3542,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Black Cat Clothes",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01400000",
|
||||
@ -4160,6 +4192,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -5848,6 +5896,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -6126,6 +6190,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -8341,6 +8421,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -9020,6 +9116,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000100",
|
||||
@ -9496,6 +9608,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Blue Attire",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01010000",
|
||||
@ -9833,6 +9961,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -14667,6 +14811,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01030000",
|
||||
@ -16119,6 +16279,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Black Cat Clothes",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01050000",
|
||||
@ -16717,6 +16893,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Black Cat Clothes",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01070000",
|
||||
@ -19745,6 +19937,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Black Cat Clothes",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01080000",
|
||||
@ -20503,6 +20711,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Blue Attire",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01010000",
|
||||
@ -21805,6 +22029,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -22340,6 +22580,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Black Cat Clothes",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01020100",
|
||||
@ -22990,6 +23246,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -23440,6 +23712,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -24660,6 +24948,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Black Cat Clothes",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01410000",
|
||||
@ -24954,6 +25258,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Black Cat Clothes",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01060000",
|
||||
@ -25286,6 +25606,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -29114,6 +29450,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Blue Attire",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01010000",
|
||||
@ -32512,6 +32864,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -32928,6 +33296,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000100",
|
||||
@ -34800,6 +35184,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Red Tunic",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01000000",
|
||||
@ -37569,6 +37969,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Blue Attire",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01010100",
|
||||
@ -41293,6 +41709,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Black Cat Clothes",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01020100",
|
||||
@ -45153,6 +45585,22 @@
|
||||
"0100F2C0115B6000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Tears of the Kingdom"
|
||||
},
|
||||
{
|
||||
"amiiboUsage": [
|
||||
{
|
||||
"Usage": "Receive the Blue Attire",
|
||||
"write": false
|
||||
},
|
||||
{
|
||||
"Usage": "Receive random materials",
|
||||
"write": false
|
||||
}
|
||||
],
|
||||
"gameID": [
|
||||
"01008CF01BAAC000"
|
||||
],
|
||||
"gameName": "The Legend of Zelda: Echoes of Wisdom"
|
||||
}
|
||||
],
|
||||
"head": "01010000",
|
||||
@ -47896,5 +48344,5 @@
|
||||
"type": "Figure"
|
||||
}
|
||||
],
|
||||
"lastUpdated": "2024-10-01T00:00:25.035619"
|
||||
}
|
||||
"lastUpdated": "2024-11-17T15:28:47.035619"
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ To merge pull requests, you must have write permissions in the repository.
|
||||
## Quick Code Review Rules
|
||||
|
||||
* Do not mix unrelated changes in one pull request. For example, a code style change should never be mixed with a bug fix.
|
||||
* All changes should follow the existing code style. You can read more about our code style at [docs/coding-guidelines](../coding-guidelines/coding-style.md).
|
||||
* All changes should follow the existing code style. You can read more about our code style at [docs/coding-style](../coding-guidelines/coding-style.md).
|
||||
* Adding external dependencies is to be avoided unless not doing so would introduce _significant_ complexity. Any dependency addition should be justified and discussed before merge.
|
||||
* Use Draft pull requests for changes you are still working on but want early CI loop feedback. When you think your changes are ready for review, [change the status](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request) of your pull request.
|
||||
* Rebase your changes when required or directly requested. Changes should always be commited on top of the upstream branch, not the other way around.
|
||||
|
@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer
|
||||
public enum MultiplayerMode
|
||||
{
|
||||
Disabled,
|
||||
LdnRyu,
|
||||
LdnMitm,
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ namespace Ryujinx.Common.Logging.Targets
|
||||
}
|
||||
|
||||
// Clean up old logs, should only keep 3
|
||||
FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray();
|
||||
FileInfo[] files = logDir.GetFiles("*.log").OrderBy(info => info.CreationTime).ToArray();
|
||||
for (int i = 0; i < files.Length - 2; i++)
|
||||
{
|
||||
try
|
||||
|
@ -803,18 +803,6 @@ namespace Ryujinx.Common.Memory
|
||||
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
|
||||
{
|
||||
T _e0;
|
||||
@ -828,6 +816,18 @@ namespace Ryujinx.Common.Memory
|
||||
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
|
||||
{
|
||||
T _e0;
|
||||
|
@ -15,6 +15,7 @@ namespace Ryujinx.Common
|
||||
private const string ConfigFileName = "%%RYUJINX_CONFIG_FILE_NAME%%";
|
||||
|
||||
public const string ReleaseChannelOwner = "%%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER%%";
|
||||
public const string ReleaseChannelSourceRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO%%";
|
||||
public const string ReleaseChannelRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_REPO%%";
|
||||
|
||||
public static string ConfigName => !ConfigFileName.StartsWith("%%") ? ConfigFileName : "Config.json";
|
||||
@ -23,6 +24,7 @@ namespace Ryujinx.Common
|
||||
!BuildGitHash.StartsWith("%%") &&
|
||||
!ReleaseChannelName.StartsWith("%%") &&
|
||||
!ReleaseChannelOwner.StartsWith("%%") &&
|
||||
!ReleaseChannelSourceRepo.StartsWith("%%") &&
|
||||
!ReleaseChannelRepo.StartsWith("%%") &&
|
||||
!ConfigFileName.StartsWith("%%");
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Common.Utilities
|
||||
{
|
||||
@ -65,6 +66,11 @@ namespace Ryujinx.Common.Utilities
|
||||
return (targetProperties, targetAddressInfo);
|
||||
}
|
||||
|
||||
public static bool SupportsDynamicDns()
|
||||
{
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
}
|
||||
|
||||
public static uint ConvertIpv4Address(IPAddress ipAddress)
|
||||
{
|
||||
return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes());
|
||||
|
@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
IPipeline Pipeline { get; }
|
||||
|
||||
IWindow Window { get; }
|
||||
|
||||
|
||||
uint ProgramCount { get; }
|
||||
|
||||
void BackgroundContextAction(Action action, bool alwaysBackground = false);
|
||||
|
@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
|
||||
{
|
||||
ProgramCount++;
|
||||
|
||||
|
||||
return new Program(shaders, info.FragmentOutputMap);
|
||||
}
|
||||
|
||||
|
@ -55,8 +55,10 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
if (_handle != BufferHandle.Null)
|
||||
{
|
||||
// May need to restride the vertex buffer.
|
||||
|
||||
if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && (_stride % alignment) != 0)
|
||||
//
|
||||
// Fix divide by zero when recovering from missed draw (Oct. 16 2024)
|
||||
// (fixes crash in 'Baldo: The Guardian Owls' opening cutscene)
|
||||
if (gd.NeedsVertexBufferAlignment(AttributeScalarAlignment, out int alignment) && alignment != 0 && (_stride % alignment) != 0)
|
||||
{
|
||||
autoBuffer = gd.BufferManager.GetAlignedVertexBuffer(cbs, _handle, _offset, _size, _stride, alignment);
|
||||
|
||||
|
@ -549,7 +549,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
public IProgram CreateProgram(ShaderSource[] sources, ShaderInfo info)
|
||||
{
|
||||
ProgramCount++;
|
||||
|
||||
|
||||
bool isCompute = sources.Length == 1 && sources[0].Stage == ShaderStage.Compute;
|
||||
|
||||
if (info.State.HasValue || isCompute)
|
||||
|
@ -164,6 +164,21 @@ namespace Ryujinx.HLE
|
||||
/// </summary>
|
||||
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>
|
||||
/// An action called when HLE force a refresh of output after docked mode changed.
|
||||
/// </summary>
|
||||
@ -194,7 +209,10 @@ namespace Ryujinx.HLE
|
||||
float audioVolume,
|
||||
bool useHypervisor,
|
||||
string multiplayerLanInterfaceId,
|
||||
MultiplayerMode multiplayerMode)
|
||||
MultiplayerMode multiplayerMode,
|
||||
bool multiplayerDisableP2p,
|
||||
string multiplayerLdnPassphrase,
|
||||
string multiplayerLdnServer)
|
||||
{
|
||||
VirtualFileSystem = virtualFileSystem;
|
||||
LibHacHorizonManager = libHacHorizonManager;
|
||||
@ -222,6 +240,9 @@ namespace Ryujinx.HLE
|
||||
UseHypervisor = useHypervisor;
|
||||
MultiplayerLanInterfaceId = multiplayerLanInterfaceId;
|
||||
MultiplayerMode = multiplayerMode;
|
||||
MultiplayerDisableP2p = multiplayerDisableP2p;
|
||||
MultiplayerLdnPassphrase = multiplayerLdnPassphrase;
|
||||
MultiplayerLdnServer = multiplayerLdnServer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService;
|
||||
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||
{
|
||||
@ -25,5 +26,14 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(350)]
|
||||
// OpenSystemApplicationProxy(u64, pid, handle<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
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x20)]
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 8)]
|
||||
struct NetworkConfig
|
||||
{
|
||||
public IntentId IntentId;
|
||||
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x60)]
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x60, Pack = 8)]
|
||||
struct ScanFilter
|
||||
{
|
||||
public NetworkId NetworkId;
|
||||
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x44)]
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x44, Pack = 2)]
|
||||
struct SecurityConfig
|
||||
{
|
||||
public SecurityMode SecurityMode;
|
||||
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x20)]
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)]
|
||||
struct SecurityParameter
|
||||
{
|
||||
public Array16<byte> Data;
|
||||
|
@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x30)]
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x30, Pack = 1)]
|
||||
struct UserConfig
|
||||
{
|
||||
public Array33<byte> UserName;
|
||||
|
@ -15,6 +15,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
public Array8<NodeLatestUpdate> LatestUpdates = new();
|
||||
public bool Connected { get; private set; }
|
||||
|
||||
public ProxyConfig Config => _parent.NetworkClient.Config;
|
||||
|
||||
public AccessPoint(IUserLocalCommunicationService parent)
|
||||
{
|
||||
_parent = parent;
|
||||
@ -24,9 +26,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_parent.NetworkClient.DisconnectNetwork();
|
||||
if (_parent?.NetworkClient != null)
|
||||
{
|
||||
_parent.NetworkClient.DisconnectNetwork();
|
||||
|
||||
_parent.NetworkClient.NetworkChange -= NetworkChanged;
|
||||
_parent.NetworkClient.NetworkChange -= NetworkChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void NetworkChanged(object sender, NetworkChangeEventArgs e)
|
||||
|
@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
{
|
||||
interface INetworkClient : IDisposable
|
||||
{
|
||||
ProxyConfig Config { get; }
|
||||
bool NeedsRealId { get; }
|
||||
|
||||
event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
||||
|
@ -9,6 +9,8 @@ using Ryujinx.HLE.HOS.Ipc;
|
||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
@ -21,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
{
|
||||
class IUserLocalCommunicationService : IpcService, IDisposable
|
||||
{
|
||||
public static string DefaultLanPlayHost = "ryuldn.vudjun.com";
|
||||
public static short LanPlayPort = 30456;
|
||||
|
||||
public INetworkClient NetworkClient { get; private set; }
|
||||
|
||||
private const int NifmRequestID = 90;
|
||||
@ -175,19 +180,37 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected)
|
||||
{
|
||||
(_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
|
||||
|
||||
if (unicastAddress == null)
|
||||
ProxyConfig config = _state switch
|
||||
{
|
||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
|
||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
|
||||
NetworkState.AccessPointCreated => _accessPoint.Config,
|
||||
NetworkState.StationConnected => _station.Config,
|
||||
|
||||
_ => default
|
||||
};
|
||||
|
||||
if (config.ProxyIp == 0)
|
||||
{
|
||||
(_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
|
||||
|
||||
if (unicastAddress == null)
|
||||
{
|
||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
|
||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
|
||||
|
||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
|
||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
|
||||
Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP.");
|
||||
|
||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
|
||||
context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
|
||||
context.ResponseData.Write(config.ProxyIp);
|
||||
context.ResponseData.Write(config.ProxySubnetMask);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -1066,6 +1089,27 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case MultiplayerMode.LdnRyu:
|
||||
try
|
||||
{
|
||||
string ldnServer = context.Device.Configuration.MultiplayerLdnServer;
|
||||
if (string.IsNullOrEmpty(ldnServer))
|
||||
{
|
||||
ldnServer = DefaultLanPlayHost;
|
||||
}
|
||||
if (!IPAddress.TryParse(ldnServer, out IPAddress ipAddress))
|
||||
{
|
||||
ipAddress = Dns.GetHostEntry(ldnServer).AddressList[0];
|
||||
}
|
||||
NetworkClient = new LdnMasterProxyClient(ipAddress.ToString(), LanPlayPort, context.Device.Configuration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.ServiceLdn, "Could not locate LdnRyu server. Defaulting to stubbed wireless.");
|
||||
Logger.Error?.Print(LogClass.ServiceLdn, ex.Message);
|
||||
NetworkClient = new LdnDisabledClient();
|
||||
}
|
||||
break;
|
||||
case MultiplayerMode.LdnMitm:
|
||||
NetworkClient = new LdnMitmClient(context.Device.Configuration);
|
||||
break;
|
||||
@ -1103,7 +1147,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
_accessPoint?.Dispose();
|
||||
_accessPoint = null;
|
||||
|
||||
NetworkClient?.Dispose();
|
||||
NetworkClient?.DisconnectAndStop();
|
||||
NetworkClient = null;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||
using System;
|
||||
@ -6,12 +7,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
{
|
||||
class LdnDisabledClient : INetworkClient
|
||||
{
|
||||
public ProxyConfig Config { get; }
|
||||
public bool NeedsRealId => true;
|
||||
|
||||
public event EventHandler<NetworkChangeEventArgs> NetworkChange;
|
||||
|
||||
public NetworkError Connect(ConnectRequest request)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!");
|
||||
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
|
||||
|
||||
return NetworkError.None;
|
||||
@ -19,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
public NetworkError ConnectPrivate(ConnectPrivateRequest request)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!");
|
||||
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
|
||||
|
||||
return NetworkError.None;
|
||||
@ -26,6 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!");
|
||||
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
|
||||
|
||||
return true;
|
||||
@ -33,6 +38,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!");
|
||||
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
|
||||
|
||||
return true;
|
||||
@ -49,6 +55,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to scan for networks, but Multiplayer is disabled!");
|
||||
return Array.Empty<NetworkInfo>();
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
|
||||
/// </summary>
|
||||
internal class LdnMitmClient : INetworkClient
|
||||
{
|
||||
public ProxyConfig Config { get; }
|
||||
public bool NeedsRealId => false;
|
||||
|
||||
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 ProxyConfig Config => _parent.NetworkClient.Config;
|
||||
|
||||
public Station(IUserLocalCommunicationService parent)
|
||||
{
|
||||
_parent = parent;
|
||||
@ -48,9 +50,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_parent.NetworkClient.DisconnectNetwork();
|
||||
if (_parent.NetworkClient != null)
|
||||
{
|
||||
_parent.NetworkClient.DisconnectNetwork();
|
||||
|
||||
_parent.NetworkClient.NetworkChange -= NetworkChanged;
|
||||
_parent.NetworkClient.NetworkChange -= NetworkChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private ResultCode NetworkErrorToResult(NetworkError error)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||
@ -14,5 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||
public UserConfig UserConfig;
|
||||
public NetworkConfig NetworkConfig;
|
||||
public AddressList AddressList;
|
||||
|
||||
public RyuNetworkConfig RyuNetworkConfig;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||
@ -6,11 +7,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
|
||||
/// <remarks>
|
||||
/// Advertise data is appended separately (remaining data in the buffer).
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)]
|
||||
[StructLayout(LayoutKind.Sequential, Size = 0xBC, Pack = 1)]
|
||||
struct CreateAccessPointRequest
|
||||
{
|
||||
public SecurityConfig SecurityConfig;
|
||||
public UserConfig UserConfig;
|
||||
public NetworkConfig NetworkConfig;
|
||||
|
||||
public RyuNetworkConfig RyuNetworkConfig;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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),
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
|
||||
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -21,21 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; }
|
||||
|
||||
public nint Handle => Socket.Handle;
|
||||
public nint Handle => IntPtr.Zero;
|
||||
|
||||
public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;
|
||||
|
||||
public IPEndPoint LocalEndPoint => Socket.LocalEndPoint as IPEndPoint;
|
||||
|
||||
public Socket Socket { get; }
|
||||
public ISocketImpl Socket { get; }
|
||||
|
||||
public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType)
|
||||
public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, string lanInterfaceId)
|
||||
{
|
||||
Socket = new Socket(addressFamily, socketType, protocolType);
|
||||
Socket = SocketHelpers.CreateSocket(addressFamily, socketType, protocolType, lanInterfaceId);
|
||||
Refcount = 1;
|
||||
}
|
||||
|
||||
private ManagedSocket(Socket socket)
|
||||
private ManagedSocket(ISocketImpl socket)
|
||||
{
|
||||
Socket = socket;
|
||||
Refcount = 1;
|
||||
@ -185,6 +186,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
}
|
||||
}
|
||||
|
||||
bool hasEmittedBlockingWarning = false;
|
||||
|
||||
public LinuxError Receive(out int receiveSize, Span<byte> buffer, BsdSocketFlags flags)
|
||||
{
|
||||
LinuxError result;
|
||||
@ -199,6 +202,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
shouldBlockAfterOperation = true;
|
||||
}
|
||||
|
||||
if (Blocking && !hasEmittedBlockingWarning)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors.");
|
||||
hasEmittedBlockingWarning = true;
|
||||
}
|
||||
|
||||
receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags));
|
||||
|
||||
result = LinuxError.SUCCESS;
|
||||
@ -236,6 +245,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
shouldBlockAfterOperation = true;
|
||||
}
|
||||
|
||||
if (Blocking && !hasEmittedBlockingWarning)
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors.");
|
||||
hasEmittedBlockingWarning = true;
|
||||
}
|
||||
|
||||
if (!Socket.IsBound)
|
||||
{
|
||||
receiveSize = -1;
|
||||
@ -313,7 +328,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported GetSockOpt Option: {option} Level: {level}");
|
||||
optionValue.Clear();
|
||||
|
||||
return LinuxError.SUCCESS;
|
||||
return LinuxError.EOPNOTSUPP;
|
||||
}
|
||||
|
||||
byte[] tempOptionValue = new byte[optionValue.Length];
|
||||
@ -347,7 +362,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported SetSockOpt Option: {option} Level: {level}");
|
||||
|
||||
return LinuxError.SUCCESS;
|
||||
return LinuxError.EOPNOTSUPP;
|
||||
}
|
||||
|
||||
int value = optionValue.Length >= 4 ? MemoryMarshal.Read<int>(optionValue) : MemoryMarshal.Read<byte>(optionValue);
|
||||
@ -493,7 +508,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
try
|
||||
{
|
||||
int receiveSize = Socket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
|
||||
int receiveSize = (Socket as DefaultSocket).BaseSocket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
|
||||
|
||||
if (receiveSize > 0)
|
||||
{
|
||||
@ -531,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
try
|
||||
{
|
||||
int sendSize = Socket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
|
||||
int sendSize = (Socket as DefaultSocket).BaseSocket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
|
||||
|
||||
if (sendSize > 0)
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
|
||||
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Sockets;
|
||||
@ -26,45 +27,46 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
public LinuxError Poll(List<PollEvent> events, int timeoutMilliseconds, out int updatedCount)
|
||||
{
|
||||
List<Socket> readEvents = new();
|
||||
List<Socket> writeEvents = new();
|
||||
List<Socket> errorEvents = new();
|
||||
List<ISocketImpl> readEvents = new();
|
||||
List<ISocketImpl> writeEvents = new();
|
||||
List<ISocketImpl> errorEvents = new();
|
||||
|
||||
updatedCount = 0;
|
||||
|
||||
foreach (PollEvent evnt in events)
|
||||
{
|
||||
ManagedSocket socket = (ManagedSocket)evnt.FileDescriptor;
|
||||
|
||||
bool isValidEvent = evnt.Data.InputEvents == 0;
|
||||
|
||||
errorEvents.Add(socket.Socket);
|
||||
|
||||
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
|
||||
if (evnt.FileDescriptor is ManagedSocket ms)
|
||||
{
|
||||
readEvents.Add(socket.Socket);
|
||||
bool isValidEvent = evnt.Data.InputEvents == 0;
|
||||
|
||||
isValidEvent = true;
|
||||
}
|
||||
errorEvents.Add(ms.Socket);
|
||||
|
||||
if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0)
|
||||
{
|
||||
readEvents.Add(socket.Socket);
|
||||
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
|
||||
{
|
||||
readEvents.Add(ms.Socket);
|
||||
|
||||
isValidEvent = true;
|
||||
}
|
||||
isValidEvent = true;
|
||||
}
|
||||
|
||||
if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0)
|
||||
{
|
||||
writeEvents.Add(socket.Socket);
|
||||
if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0)
|
||||
{
|
||||
readEvents.Add(ms.Socket);
|
||||
|
||||
isValidEvent = true;
|
||||
}
|
||||
isValidEvent = true;
|
||||
}
|
||||
|
||||
if (!isValidEvent)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}");
|
||||
return LinuxError.EINVAL;
|
||||
if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0)
|
||||
{
|
||||
writeEvents.Add(ms.Socket);
|
||||
|
||||
isValidEvent = true;
|
||||
}
|
||||
|
||||
if (!isValidEvent)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}");
|
||||
return LinuxError.EINVAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +74,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
{
|
||||
int actualTimeoutMicroseconds = timeoutMilliseconds == -1 ? -1 : timeoutMilliseconds * 1000;
|
||||
|
||||
Socket.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds);
|
||||
SocketHelpers.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds);
|
||||
}
|
||||
catch (SocketException exception)
|
||||
{
|
||||
@ -81,34 +83,37 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
foreach (PollEvent evnt in events)
|
||||
{
|
||||
Socket socket = ((ManagedSocket)evnt.FileDescriptor).Socket;
|
||||
|
||||
PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents;
|
||||
|
||||
if (errorEvents.Contains(socket))
|
||||
if (evnt.FileDescriptor is ManagedSocket ms)
|
||||
{
|
||||
outputEvents |= PollEventTypeMask.Error;
|
||||
ISocketImpl socket = ms.Socket;
|
||||
|
||||
if (!socket.Connected || !socket.IsBound)
|
||||
PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents;
|
||||
|
||||
if (errorEvents.Contains(ms.Socket))
|
||||
{
|
||||
outputEvents |= PollEventTypeMask.Disconnected;
|
||||
}
|
||||
}
|
||||
outputEvents |= PollEventTypeMask.Error;
|
||||
|
||||
if (readEvents.Contains(socket))
|
||||
{
|
||||
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
|
||||
if (!socket.Connected || !socket.IsBound)
|
||||
{
|
||||
outputEvents |= PollEventTypeMask.Disconnected;
|
||||
}
|
||||
}
|
||||
|
||||
if (readEvents.Contains(ms.Socket))
|
||||
{
|
||||
outputEvents |= PollEventTypeMask.Input;
|
||||
if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
|
||||
{
|
||||
outputEvents |= PollEventTypeMask.Input;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (writeEvents.Contains(socket))
|
||||
{
|
||||
outputEvents |= PollEventTypeMask.Output;
|
||||
}
|
||||
if (writeEvents.Contains(ms.Socket))
|
||||
{
|
||||
outputEvents |= PollEventTypeMask.Output;
|
||||
}
|
||||
|
||||
evnt.Data.OutputEvents = outputEvents;
|
||||
evnt.Data.OutputEvents = outputEvents;
|
||||
}
|
||||
}
|
||||
|
||||
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
|
||||
@ -118,53 +123,55 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
public LinuxError Select(List<PollEvent> events, int timeout, out int updatedCount)
|
||||
{
|
||||
List<Socket> readEvents = new();
|
||||
List<Socket> writeEvents = new();
|
||||
List<Socket> errorEvents = new();
|
||||
List<ISocketImpl> readEvents = new();
|
||||
List<ISocketImpl> writeEvents = new();
|
||||
List<ISocketImpl> errorEvents = new();
|
||||
|
||||
updatedCount = 0;
|
||||
|
||||
foreach (PollEvent pollEvent in events)
|
||||
{
|
||||
ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor;
|
||||
|
||||
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input))
|
||||
if (pollEvent.FileDescriptor is ManagedSocket ms)
|
||||
{
|
||||
readEvents.Add(socket.Socket);
|
||||
}
|
||||
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input))
|
||||
{
|
||||
readEvents.Add(ms.Socket);
|
||||
}
|
||||
|
||||
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output))
|
||||
{
|
||||
writeEvents.Add(socket.Socket);
|
||||
}
|
||||
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output))
|
||||
{
|
||||
writeEvents.Add(ms.Socket);
|
||||
}
|
||||
|
||||
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error))
|
||||
{
|
||||
errorEvents.Add(socket.Socket);
|
||||
if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error))
|
||||
{
|
||||
errorEvents.Add(ms.Socket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Socket.Select(readEvents, writeEvents, errorEvents, timeout);
|
||||
SocketHelpers.Select(readEvents, writeEvents, errorEvents, timeout);
|
||||
|
||||
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
|
||||
|
||||
foreach (PollEvent pollEvent in events)
|
||||
{
|
||||
ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor;
|
||||
|
||||
if (readEvents.Contains(socket.Socket))
|
||||
if (pollEvent.FileDescriptor is ManagedSocket ms)
|
||||
{
|
||||
pollEvent.Data.OutputEvents |= PollEventTypeMask.Input;
|
||||
}
|
||||
if (readEvents.Contains(ms.Socket))
|
||||
{
|
||||
pollEvent.Data.OutputEvents |= PollEventTypeMask.Input;
|
||||
}
|
||||
|
||||
if (writeEvents.Contains(socket.Socket))
|
||||
{
|
||||
pollEvent.Data.OutputEvents |= PollEventTypeMask.Output;
|
||||
}
|
||||
if (writeEvents.Contains(ms.Socket))
|
||||
{
|
||||
pollEvent.Data.OutputEvents |= PollEventTypeMask.Output;
|
||||
}
|
||||
|
||||
if (errorEvents.Contains(socket.Socket))
|
||||
{
|
||||
pollEvent.Data.OutputEvents |= PollEventTypeMask.Error;
|
||||
if (errorEvents.Contains(ms.Socket))
|
||||
{
|
||||
pollEvent.Data.OutputEvents |= PollEventTypeMask.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
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);
|
||||
|
||||
if (!context.Device.Configuration.EnableInternetAccess)
|
||||
if (host != "localhost" && !context.Device.Configuration.EnableInternetAccess)
|
||||
{
|
||||
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.Impl;
|
||||
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
|
||||
using Ryujinx.HLE.HOS.Services.Ssl.Types;
|
||||
using System;
|
||||
using System.IO;
|
||||
@ -116,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService
|
||||
public ResultCode Handshake(string hostName)
|
||||
{
|
||||
StartSslOperation();
|
||||
_stream = new SslStream(new NetworkStream(((ManagedSocket)Socket).Socket, false), false, null, null);
|
||||
_stream = new SslStream(new NetworkStream(((DefaultSocket)((ManagedSocket)Socket).Socket).BaseSocket, false), false, null, null);
|
||||
hostName = RetrieveHostName(hostName);
|
||||
_stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false);
|
||||
EndSslOperation();
|
||||
|
@ -85,8 +85,8 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
}
|
||||
|
||||
// TODO: LibHac npdm currently doesn't support version field.
|
||||
string version = ProgramId > 0x0100000000007FFF
|
||||
? DisplayVersion
|
||||
string version = ProgramId > 0x0100000000007FFF
|
||||
? DisplayVersion
|
||||
: device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?";
|
||||
|
||||
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]");
|
||||
|
@ -29,6 +29,7 @@
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||
<PackageReference Include="NetCoreServer" />
|
||||
<PackageReference Include="Open.NAT.Core" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -577,7 +577,10 @@ namespace Ryujinx.Headless.SDL2
|
||||
options.AudioVolume,
|
||||
options.UseHypervisor ?? true,
|
||||
options.MultiplayerLanInterfaceId,
|
||||
Common.Configuration.Multiplayer.MultiplayerMode.Disabled);
|
||||
Common.Configuration.Multiplayer.MultiplayerMode.Disabled,
|
||||
false,
|
||||
"",
|
||||
"");
|
||||
|
||||
return new Switch(configuration);
|
||||
}
|
||||
|
@ -115,7 +115,10 @@ namespace Ryujinx.Input.SDL2
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_gamepadsIds.Insert(joystickDeviceId, id);
|
||||
if (joystickDeviceId <= _gamepadsIds.FindLastIndex(_ => true))
|
||||
_gamepadsIds.Insert(joystickDeviceId, id);
|
||||
else
|
||||
_gamepadsIds.Add(id);
|
||||
}
|
||||
|
||||
OnGamepadConnected?.Invoke(id);
|
||||
|
@ -27,6 +27,8 @@ namespace Ryujinx.UI.App.Common
|
||||
public ulong Id { get; set; }
|
||||
public string Developer { get; set; } = "Unknown";
|
||||
public string Version { get; set; } = "0";
|
||||
public int PlayerCount { get; set; }
|
||||
public int GameCount { get; set; }
|
||||
public TimeSpan TimePlayed { get; set; }
|
||||
public DateTime? LastPlayed { get; set; }
|
||||
public string FileExtension { get; set; }
|
||||
|
@ -12,6 +12,7 @@ using LibHac.Tools.Fs;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
@ -27,10 +28,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ContentType = LibHac.Ncm.ContentType;
|
||||
using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
|
||||
using Path = System.IO.Path;
|
||||
@ -41,8 +44,10 @@ namespace Ryujinx.UI.App.Common
|
||||
{
|
||||
public class ApplicationLibrary
|
||||
{
|
||||
public static string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
|
||||
public Language DesiredLanguage { get; set; }
|
||||
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
|
||||
public event EventHandler<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
|
||||
|
||||
public readonly IObservableCache<ApplicationData, ulong> Applications;
|
||||
public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates;
|
||||
@ -62,6 +67,7 @@ namespace Ryujinx.UI.App.Common
|
||||
private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc);
|
||||
|
||||
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
|
||||
{
|
||||
@ -687,7 +693,7 @@ namespace Ryujinx.UI.App.Common
|
||||
(Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0) ||
|
||||
(Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI) ||
|
||||
(Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA) ||
|
||||
(Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) ||
|
||||
(Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) ||
|
||||
(Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO)
|
||||
);
|
||||
|
||||
@ -719,6 +725,7 @@ namespace Ryujinx.UI.App.Common
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
|
||||
foreach (string applicationPath in applicationPaths)
|
||||
{
|
||||
@ -775,6 +782,46 @@ namespace Ryujinx.UI.App.Common
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshLdn()
|
||||
{
|
||||
|
||||
if (ConfigurationState.Instance.Multiplayer.Mode == MultiplayerMode.LdnRyu)
|
||||
{
|
||||
try
|
||||
{
|
||||
string ldnWebHost = ConfigurationState.Instance.Multiplayer.LdnServer;
|
||||
if (string.IsNullOrEmpty(ldnWebHost))
|
||||
{
|
||||
ldnWebHost = DefaultLanPlayWebHost;
|
||||
}
|
||||
IEnumerable<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.
|
||||
public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs)
|
||||
{
|
||||
|
16
src/Ryujinx.UI.Common/App/LdnGameData.cs
Normal file
16
src/Ryujinx.UI.Common/App/LdnGameData.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.UI.App.Common
|
||||
{
|
||||
public struct LdnGameData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public int PlayerCount { get; set; }
|
||||
public int MaxPlayerCount { get; set; }
|
||||
public string GameName { get; set; }
|
||||
public string TitleId { get; set; }
|
||||
public string Mode { get; set; }
|
||||
public string Status { get; set; }
|
||||
public IEnumerable<string> Players { get; set; }
|
||||
}
|
||||
}
|
10
src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs
Normal file
10
src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.UI.App.Common
|
||||
{
|
||||
public class LdnGameDataReceivedEventArgs : EventArgs
|
||||
{
|
||||
public IEnumerable<LdnGameData> LdnData { get; set; }
|
||||
}
|
||||
}
|
11
src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs
Normal file
11
src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.UI.App.Common
|
||||
{
|
||||
[JsonSerializable(typeof(IEnumerable<LdnGameData>))]
|
||||
internal partial class LdnGameDataSerializerContext : JsonSerializerContext
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -392,6 +392,21 @@ namespace Ryujinx.UI.Common.Configuration
|
||||
/// </summary>
|
||||
public string MultiplayerLanInterfaceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disable P2p Toggle
|
||||
/// </summary>
|
||||
public bool MultiplayerDisableP2p { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Local network passphrase, for private networks.
|
||||
/// </summary>
|
||||
public string MultiplayerLdnPassphrase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom LDN Server
|
||||
/// </summary>
|
||||
public string LdnServer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Uses Hypervisor over JIT if available
|
||||
/// </summary>
|
||||
|
@ -0,0 +1,718 @@
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.UI.Common.Configuration.System;
|
||||
using Ryujinx.UI.Common.Configuration.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.UI.Common.Configuration
|
||||
{
|
||||
public partial class ConfigurationState
|
||||
{
|
||||
public void Load(ConfigurationFileFormat configurationFileFormat, string configurationFilePath)
|
||||
{
|
||||
bool configurationFileUpdated = false;
|
||||
|
||||
if (configurationFileFormat.Version is < 0 or > ConfigurationFileFormat.CurrentVersion)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Unsupported configuration version {configurationFileFormat.Version}, loading default.");
|
||||
|
||||
LoadDefault();
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 2)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 2.");
|
||||
|
||||
configurationFileFormat.SystemRegion = Region.USA;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 3)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 3.");
|
||||
|
||||
configurationFileFormat.SystemTimeZone = "UTC";
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 4)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 4.");
|
||||
|
||||
configurationFileFormat.MaxAnisotropy = -1;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 5)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 5.");
|
||||
|
||||
configurationFileFormat.SystemTimeOffset = 0;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 8)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 8.");
|
||||
|
||||
configurationFileFormat.EnablePtc = true;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 9)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 9.");
|
||||
|
||||
configurationFileFormat.ColumnSort = new ColumnSort
|
||||
{
|
||||
SortColumnId = 0,
|
||||
SortAscending = false,
|
||||
};
|
||||
|
||||
configurationFileFormat.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVsync = Key.F1,
|
||||
};
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 10)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 10.");
|
||||
|
||||
configurationFileFormat.AudioBackend = AudioBackend.OpenAl;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 11)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 11.");
|
||||
|
||||
configurationFileFormat.ResScale = 1;
|
||||
configurationFileFormat.ResScaleCustom = 1.0f;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 12)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 12.");
|
||||
|
||||
configurationFileFormat.LoggingGraphicsDebugLevel = GraphicsDebugLevel.None;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
// configurationFileFormat.Version == 13 -> LDN1
|
||||
|
||||
if (configurationFileFormat.Version < 14)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 14.");
|
||||
|
||||
configurationFileFormat.CheckUpdatesOnStart = true;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 16)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 16.");
|
||||
|
||||
configurationFileFormat.EnableShaderCache = true;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 17)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 17.");
|
||||
|
||||
configurationFileFormat.StartFullscreen = false;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 18)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 18.");
|
||||
|
||||
configurationFileFormat.AspectRatio = AspectRatio.Fixed16x9;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
// configurationFileFormat.Version == 19 -> LDN2
|
||||
|
||||
if (configurationFileFormat.Version < 20)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 20.");
|
||||
|
||||
configurationFileFormat.ShowConfirmExit = true;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 21)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 21.");
|
||||
|
||||
// Initialize network config.
|
||||
|
||||
configurationFileFormat.MultiplayerMode = MultiplayerMode.Disabled;
|
||||
configurationFileFormat.MultiplayerLanInterfaceId = "0";
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 22)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22.");
|
||||
|
||||
configurationFileFormat.HideCursor = HideCursorMode.Never;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 24)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 24.");
|
||||
|
||||
configurationFileFormat.InputConfig = new List<InputConfig>
|
||||
{
|
||||
new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = "0",
|
||||
PlayerIndex = PlayerIndex.Player1,
|
||||
ControllerType = ControllerType.ProController,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 25)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25.");
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 26)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26.");
|
||||
|
||||
configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 27)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27.");
|
||||
|
||||
configurationFileFormat.EnableMouse = false;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 28)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28.");
|
||||
|
||||
configurationFileFormat.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVsync = Key.F1,
|
||||
Screenshot = Key.F8,
|
||||
};
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 29)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29.");
|
||||
|
||||
configurationFileFormat.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVsync = Key.F1,
|
||||
Screenshot = Key.F8,
|
||||
ShowUI = Key.F4,
|
||||
};
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 30)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30.");
|
||||
|
||||
foreach (InputConfig config in configurationFileFormat.InputConfig)
|
||||
{
|
||||
if (config is StandardControllerInputConfig controllerConfig)
|
||||
{
|
||||
controllerConfig.Rumble = new RumbleConfigController
|
||||
{
|
||||
EnableRumble = false,
|
||||
StrongRumble = 1f,
|
||||
WeakRumble = 1f,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 31)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31.");
|
||||
|
||||
configurationFileFormat.BackendThreading = BackendThreading.Auto;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 32)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32.");
|
||||
|
||||
configurationFileFormat.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
|
||||
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
|
||||
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
|
||||
Pause = Key.F5,
|
||||
};
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 33)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33.");
|
||||
|
||||
configurationFileFormat.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
|
||||
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
|
||||
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
|
||||
Pause = configurationFileFormat.Hotkeys.Pause,
|
||||
ToggleMute = Key.F2,
|
||||
};
|
||||
|
||||
configurationFileFormat.AudioVolume = 1;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 34)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34.");
|
||||
|
||||
configurationFileFormat.EnableInternetAccess = false;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 35)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35.");
|
||||
|
||||
foreach (InputConfig config in configurationFileFormat.InputConfig)
|
||||
{
|
||||
if (config is StandardControllerInputConfig controllerConfig)
|
||||
{
|
||||
controllerConfig.RangeLeft = 1.0f;
|
||||
controllerConfig.RangeRight = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 36)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36.");
|
||||
|
||||
configurationFileFormat.LoggingEnableTrace = false;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 37)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37.");
|
||||
|
||||
configurationFileFormat.ShowConsole = true;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 38)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38.");
|
||||
|
||||
configurationFileFormat.BaseStyle = "Dark";
|
||||
configurationFileFormat.GameListViewMode = 0;
|
||||
configurationFileFormat.ShowNames = true;
|
||||
configurationFileFormat.GridSize = 2;
|
||||
configurationFileFormat.LanguageCode = "en_US";
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 39)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39.");
|
||||
|
||||
configurationFileFormat.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
|
||||
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
|
||||
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
|
||||
Pause = configurationFileFormat.Hotkeys.Pause,
|
||||
ToggleMute = configurationFileFormat.Hotkeys.ToggleMute,
|
||||
ResScaleUp = Key.Unbound,
|
||||
ResScaleDown = Key.Unbound,
|
||||
};
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 40)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40.");
|
||||
|
||||
configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 41)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41.");
|
||||
|
||||
configurationFileFormat.Hotkeys = new KeyboardHotkeys
|
||||
{
|
||||
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
|
||||
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
|
||||
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
|
||||
Pause = configurationFileFormat.Hotkeys.Pause,
|
||||
ToggleMute = configurationFileFormat.Hotkeys.ToggleMute,
|
||||
ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp,
|
||||
ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown,
|
||||
VolumeUp = Key.Unbound,
|
||||
VolumeDown = Key.Unbound,
|
||||
};
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 42)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42.");
|
||||
|
||||
configurationFileFormat.EnableMacroHLE = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 43)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43.");
|
||||
|
||||
configurationFileFormat.UseHypervisor = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 44)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44.");
|
||||
|
||||
configurationFileFormat.AntiAliasing = AntiAliasing.None;
|
||||
configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear;
|
||||
configurationFileFormat.ScalingFilterLevel = 80;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 45)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45.");
|
||||
|
||||
configurationFileFormat.ShownFileTypes = new ShownFileTypes
|
||||
{
|
||||
NSP = true,
|
||||
PFS0 = true,
|
||||
XCI = true,
|
||||
NCA = true,
|
||||
NRO = true,
|
||||
NSO = true,
|
||||
};
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 46)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46.");
|
||||
|
||||
configurationFileFormat.MultiplayerLanInterfaceId = "0";
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 47)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47.");
|
||||
|
||||
configurationFileFormat.WindowStartup = new WindowStartup
|
||||
{
|
||||
WindowPositionX = 0,
|
||||
WindowPositionY = 0,
|
||||
WindowSizeHeight = 760,
|
||||
WindowSizeWidth = 1280,
|
||||
WindowMaximized = false,
|
||||
};
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 48)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48.");
|
||||
|
||||
configurationFileFormat.EnableColorSpacePassthrough = false;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 49)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49.");
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
AppDataManager.FixMacOSConfigurationFolders();
|
||||
}
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 50)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 50.");
|
||||
|
||||
configurationFileFormat.EnableHardwareAcceleration = true;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 51)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51.");
|
||||
|
||||
configurationFileFormat.RememberWindowState = true;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 52)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52.");
|
||||
|
||||
configurationFileFormat.AutoloadDirs = [];
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 53)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53.");
|
||||
|
||||
configurationFileFormat.EnableLowPowerPtc = false;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 54)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 54.");
|
||||
|
||||
configurationFileFormat.DramSize = MemoryConfiguration.MemoryConfiguration4GiB;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 55)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 55.");
|
||||
|
||||
configurationFileFormat.IgnoreApplet = false;
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
if (configurationFileFormat.Version < 56)
|
||||
{
|
||||
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 56.");
|
||||
|
||||
configurationFileFormat.ShowTitleBar = !OperatingSystem.IsWindows();
|
||||
|
||||
configurationFileUpdated = true;
|
||||
}
|
||||
|
||||
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
|
||||
Graphics.ResScale.Value = configurationFileFormat.ResScale;
|
||||
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
|
||||
Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy;
|
||||
Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio;
|
||||
Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath;
|
||||
Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading;
|
||||
Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend;
|
||||
Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu;
|
||||
Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing;
|
||||
Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter;
|
||||
Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel;
|
||||
Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug;
|
||||
Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub;
|
||||
Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo;
|
||||
Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn;
|
||||
Logger.EnableError.Value = configurationFileFormat.LoggingEnableError;
|
||||
Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace;
|
||||
Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest;
|
||||
Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog;
|
||||
Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses;
|
||||
Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel;
|
||||
System.Language.Value = configurationFileFormat.SystemLanguage;
|
||||
System.Region.Value = configurationFileFormat.SystemRegion;
|
||||
System.TimeZone.Value = configurationFileFormat.SystemTimeZone;
|
||||
System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset;
|
||||
System.EnableDockedMode.Value = configurationFileFormat.DockedMode;
|
||||
EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration;
|
||||
CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart;
|
||||
ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit;
|
||||
IgnoreApplet.Value = configurationFileFormat.IgnoreApplet;
|
||||
RememberWindowState.Value = configurationFileFormat.RememberWindowState;
|
||||
ShowTitleBar.Value = configurationFileFormat.ShowTitleBar;
|
||||
EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration;
|
||||
HideCursor.Value = configurationFileFormat.HideCursor;
|
||||
Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync;
|
||||
Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache;
|
||||
Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression;
|
||||
Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE;
|
||||
Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough;
|
||||
System.EnablePtc.Value = configurationFileFormat.EnablePtc;
|
||||
System.EnableLowPowerPtc.Value = configurationFileFormat.EnableLowPowerPtc;
|
||||
System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess;
|
||||
System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks;
|
||||
System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode;
|
||||
System.AudioBackend.Value = configurationFileFormat.AudioBackend;
|
||||
System.AudioVolume.Value = configurationFileFormat.AudioVolume;
|
||||
System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode;
|
||||
System.DramSize.Value = configurationFileFormat.DramSize;
|
||||
System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices;
|
||||
System.UseHypervisor.Value = configurationFileFormat.UseHypervisor;
|
||||
UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn;
|
||||
UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn;
|
||||
UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn;
|
||||
UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn;
|
||||
UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn;
|
||||
UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn;
|
||||
UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn;
|
||||
UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn;
|
||||
UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn;
|
||||
UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn;
|
||||
UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId;
|
||||
UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending;
|
||||
UI.GameDirs.Value = configurationFileFormat.GameDirs;
|
||||
UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs ?? [];
|
||||
UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP;
|
||||
UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0;
|
||||
UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI;
|
||||
UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA;
|
||||
UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO;
|
||||
UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO;
|
||||
UI.LanguageCode.Value = configurationFileFormat.LanguageCode;
|
||||
UI.BaseStyle.Value = configurationFileFormat.BaseStyle;
|
||||
UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode;
|
||||
UI.ShowNames.Value = configurationFileFormat.ShowNames;
|
||||
UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder;
|
||||
UI.GridSize.Value = configurationFileFormat.GridSize;
|
||||
UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort;
|
||||
UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen;
|
||||
UI.ShowConsole.Value = configurationFileFormat.ShowConsole;
|
||||
UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth;
|
||||
UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight;
|
||||
UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX;
|
||||
UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY;
|
||||
UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized;
|
||||
Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard;
|
||||
Hid.EnableMouse.Value = configurationFileFormat.EnableMouse;
|
||||
Hid.Hotkeys.Value = configurationFileFormat.Hotkeys;
|
||||
Hid.InputConfig.Value = configurationFileFormat.InputConfig ?? [];
|
||||
|
||||
Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId;
|
||||
Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode;
|
||||
Multiplayer.DisableP2p.Value = configurationFileFormat.MultiplayerDisableP2p;
|
||||
Multiplayer.LdnPassphrase.Value = configurationFileFormat.MultiplayerLdnPassphrase;
|
||||
Multiplayer.LdnServer.Value = configurationFileFormat.LdnServer;
|
||||
|
||||
if (configurationFileUpdated)
|
||||
{
|
||||
ToFileFormat().SaveConfig(configurationFilePath);
|
||||
|
||||
Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
700
src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs
Normal file
700
src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs
Normal file
@ -0,0 +1,700 @@
|
||||
using ARMeilleure;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Multiplayer;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.UI.Common.Configuration.System;
|
||||
using Ryujinx.UI.Common.Helper;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.UI.Common.Configuration
|
||||
{
|
||||
public partial class ConfigurationState
|
||||
{
|
||||
/// <summary>
|
||||
/// UI configuration section
|
||||
/// </summary>
|
||||
public class UISection
|
||||
{
|
||||
public class Columns
|
||||
{
|
||||
public ReactiveObject<bool> FavColumn { get; private set; }
|
||||
public ReactiveObject<bool> IconColumn { get; private set; }
|
||||
public ReactiveObject<bool> AppColumn { get; private set; }
|
||||
public ReactiveObject<bool> DevColumn { get; private set; }
|
||||
public ReactiveObject<bool> VersionColumn { get; private set; }
|
||||
public ReactiveObject<bool> LdnInfoColumn { get; private set; }
|
||||
public ReactiveObject<bool> TimePlayedColumn { get; private set; }
|
||||
public ReactiveObject<bool> LastPlayedColumn { get; private set; }
|
||||
public ReactiveObject<bool> FileExtColumn { get; private set; }
|
||||
public ReactiveObject<bool> FileSizeColumn { get; private set; }
|
||||
public ReactiveObject<bool> PathColumn { get; private set; }
|
||||
|
||||
public Columns()
|
||||
{
|
||||
FavColumn = new ReactiveObject<bool>();
|
||||
IconColumn = new ReactiveObject<bool>();
|
||||
AppColumn = new ReactiveObject<bool>();
|
||||
DevColumn = new ReactiveObject<bool>();
|
||||
VersionColumn = new ReactiveObject<bool>();
|
||||
LdnInfoColumn = new ReactiveObject<bool>();
|
||||
TimePlayedColumn = new ReactiveObject<bool>();
|
||||
LastPlayedColumn = new ReactiveObject<bool>();
|
||||
FileExtColumn = new ReactiveObject<bool>();
|
||||
FileSizeColumn = new ReactiveObject<bool>();
|
||||
PathColumn = new ReactiveObject<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ColumnSortSettings
|
||||
{
|
||||
public ReactiveObject<int> SortColumnId { get; private set; }
|
||||
public ReactiveObject<bool> SortAscending { get; private set; }
|
||||
|
||||
public ColumnSortSettings()
|
||||
{
|
||||
SortColumnId = new ReactiveObject<int>();
|
||||
SortAscending = new ReactiveObject<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to toggle which file types are shown in the UI
|
||||
/// </summary>
|
||||
public class ShownFileTypeSettings
|
||||
{
|
||||
public ReactiveObject<bool> NSP { get; private set; }
|
||||
public ReactiveObject<bool> PFS0 { get; private set; }
|
||||
public ReactiveObject<bool> XCI { get; private set; }
|
||||
public ReactiveObject<bool> NCA { get; private set; }
|
||||
public ReactiveObject<bool> NRO { get; private set; }
|
||||
public ReactiveObject<bool> NSO { get; private set; }
|
||||
|
||||
public ShownFileTypeSettings()
|
||||
{
|
||||
NSP = new ReactiveObject<bool>();
|
||||
PFS0 = new ReactiveObject<bool>();
|
||||
XCI = new ReactiveObject<bool>();
|
||||
NCA = new ReactiveObject<bool>();
|
||||
NRO = new ReactiveObject<bool>();
|
||||
NSO = new ReactiveObject<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
// <summary>
|
||||
/// Determines main window start-up position, size and state
|
||||
///<summary>
|
||||
public class WindowStartupSettings
|
||||
{
|
||||
public ReactiveObject<int> WindowSizeWidth { get; private set; }
|
||||
public ReactiveObject<int> WindowSizeHeight { get; private set; }
|
||||
public ReactiveObject<int> WindowPositionX { get; private set; }
|
||||
public ReactiveObject<int> WindowPositionY { get; private set; }
|
||||
public ReactiveObject<bool> WindowMaximized { get; private set; }
|
||||
|
||||
public WindowStartupSettings()
|
||||
{
|
||||
WindowSizeWidth = new ReactiveObject<int>();
|
||||
WindowSizeHeight = new ReactiveObject<int>();
|
||||
WindowPositionX = new ReactiveObject<int>();
|
||||
WindowPositionY = new ReactiveObject<int>();
|
||||
WindowMaximized = new ReactiveObject<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to toggle columns in the GUI
|
||||
/// </summary>
|
||||
public Columns GuiColumns { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to configure column sort settings in the GUI
|
||||
/// </summary>
|
||||
public ColumnSortSettings ColumnSort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of directories containing games to be used to load games into the games list
|
||||
/// </summary>
|
||||
public ReactiveObject<List<string>> GameDirs { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of directories containing DLC/updates the user wants to autoload during library refreshes
|
||||
/// </summary>
|
||||
public ReactiveObject<List<string>> AutoloadDirs { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of file types to be hidden in the games List
|
||||
/// </summary>
|
||||
public ShownFileTypeSettings ShownFileTypes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines main window start-up position, size and state
|
||||
/// </summary>
|
||||
public WindowStartupSettings WindowStartup { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Language Code for the UI
|
||||
/// </summary>
|
||||
public ReactiveObject<string> LanguageCode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Selects the base style
|
||||
/// </summary>
|
||||
public ReactiveObject<string> BaseStyle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Start games in fullscreen mode
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> StartFullscreen { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hide / Show Console Window
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowConsole { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// View Mode of the Game list
|
||||
/// </summary>
|
||||
public ReactiveObject<int> GameListViewMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show application name in Grid Mode
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowNames { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets App Icon Size in Grid Mode
|
||||
/// </summary>
|
||||
public ReactiveObject<int> GridSize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sorts Apps in Grid Mode
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ApplicationSort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets if Grid is ordered in Ascending Order
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> IsAscendingOrder { get; private set; }
|
||||
|
||||
public UISection()
|
||||
{
|
||||
GuiColumns = new Columns();
|
||||
ColumnSort = new ColumnSortSettings();
|
||||
GameDirs = new ReactiveObject<List<string>>();
|
||||
AutoloadDirs = new ReactiveObject<List<string>>();
|
||||
ShownFileTypes = new ShownFileTypeSettings();
|
||||
WindowStartup = new WindowStartupSettings();
|
||||
BaseStyle = new ReactiveObject<string>();
|
||||
StartFullscreen = new ReactiveObject<bool>();
|
||||
GameListViewMode = new ReactiveObject<int>();
|
||||
ShowNames = new ReactiveObject<bool>();
|
||||
GridSize = new ReactiveObject<int>();
|
||||
ApplicationSort = new ReactiveObject<int>();
|
||||
IsAscendingOrder = new ReactiveObject<bool>();
|
||||
LanguageCode = new ReactiveObject<string>();
|
||||
ShowConsole = new ReactiveObject<bool>();
|
||||
ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logger configuration section
|
||||
/// </summary>
|
||||
public class LoggerSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables printing debug log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableDebug { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing stub log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableStub { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing info log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableInfo { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing warning log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableWarn { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing error log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableError { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing trace log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableTrace { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing guest log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableGuest { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables printing FS access log messages
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableFsAccessLog { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls which log messages are written to the log targets
|
||||
/// </summary>
|
||||
public ReactiveObject<LogClass[]> FilteredClasses { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables logging to a file on disk
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableFileLog { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls which OpenGL log messages are recorded in the log
|
||||
/// </summary>
|
||||
public ReactiveObject<GraphicsDebugLevel> GraphicsDebugLevel { get; private set; }
|
||||
|
||||
public LoggerSection()
|
||||
{
|
||||
EnableDebug = new ReactiveObject<bool>();
|
||||
EnableDebug.LogChangesToValue(nameof(EnableDebug));
|
||||
EnableStub = new ReactiveObject<bool>();
|
||||
EnableInfo = new ReactiveObject<bool>();
|
||||
EnableWarn = new ReactiveObject<bool>();
|
||||
EnableError = new ReactiveObject<bool>();
|
||||
EnableTrace = new ReactiveObject<bool>();
|
||||
EnableGuest = new ReactiveObject<bool>();
|
||||
EnableFsAccessLog = new ReactiveObject<bool>();
|
||||
FilteredClasses = new ReactiveObject<LogClass[]>();
|
||||
EnableFileLog = new ReactiveObject<bool>();
|
||||
EnableFileLog.LogChangesToValue(nameof(EnableFileLog));
|
||||
GraphicsDebugLevel = new ReactiveObject<GraphicsDebugLevel>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System configuration section
|
||||
/// </summary>
|
||||
public class SystemSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Change System Language
|
||||
/// </summary>
|
||||
public ReactiveObject<Language> Language { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System Region
|
||||
/// </summary>
|
||||
public ReactiveObject<Region> Region { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Change System TimeZone
|
||||
/// </summary>
|
||||
public ReactiveObject<string> TimeZone { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// System Time Offset in Seconds
|
||||
/// </summary>
|
||||
public ReactiveObject<long> SystemTimeOffset { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Docked Mode
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableDockedMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables persistent profiled translation cache
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnablePtc { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables low-power persistent profiled translation cache loading
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableLowPowerPtc { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables guest Internet access
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableInternetAccess { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables integrity checks on Game content files
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableFsIntegrityChecks { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables FS access log output to the console. Possible modes are 0-3
|
||||
/// </summary>
|
||||
public ReactiveObject<int> FsGlobalAccessLogMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected audio backend
|
||||
/// </summary>
|
||||
public ReactiveObject<AudioBackend> AudioBackend { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The audio backend volume
|
||||
/// </summary>
|
||||
public ReactiveObject<float> AudioVolume { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The selected memory manager mode
|
||||
/// </summary>
|
||||
public ReactiveObject<MemoryManagerMode> MemoryManagerMode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defines the amount of RAM available on the emulated system, and how it is distributed
|
||||
/// </summary>
|
||||
public ReactiveObject<MemoryConfiguration> DramSize { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable ignoring missing services
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> IgnoreMissingServices { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Uses Hypervisor over JIT if available
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> UseHypervisor { get; private set; }
|
||||
|
||||
public SystemSection()
|
||||
{
|
||||
Language = new ReactiveObject<Language>();
|
||||
Language.LogChangesToValue(nameof(Language));
|
||||
Region = new ReactiveObject<Region>();
|
||||
Region.LogChangesToValue(nameof(Region));
|
||||
TimeZone = new ReactiveObject<string>();
|
||||
TimeZone.LogChangesToValue(nameof(TimeZone));
|
||||
SystemTimeOffset = new ReactiveObject<long>();
|
||||
SystemTimeOffset.LogChangesToValue(nameof(SystemTimeOffset));
|
||||
EnableDockedMode = new ReactiveObject<bool>();
|
||||
EnableDockedMode.LogChangesToValue(nameof(EnableDockedMode));
|
||||
EnablePtc = new ReactiveObject<bool>();
|
||||
EnablePtc.LogChangesToValue(nameof(EnablePtc));
|
||||
EnableLowPowerPtc = new ReactiveObject<bool>();
|
||||
EnableLowPowerPtc.LogChangesToValue(nameof(EnableLowPowerPtc));
|
||||
EnableLowPowerPtc.Event += (_, evnt)
|
||||
=> Optimizations.LowPower = evnt.NewValue;
|
||||
EnableInternetAccess = new ReactiveObject<bool>();
|
||||
EnableInternetAccess.LogChangesToValue(nameof(EnableInternetAccess));
|
||||
EnableFsIntegrityChecks = new ReactiveObject<bool>();
|
||||
EnableFsIntegrityChecks.LogChangesToValue(nameof(EnableFsIntegrityChecks));
|
||||
FsGlobalAccessLogMode = new ReactiveObject<int>();
|
||||
FsGlobalAccessLogMode.LogChangesToValue(nameof(FsGlobalAccessLogMode));
|
||||
AudioBackend = new ReactiveObject<AudioBackend>();
|
||||
AudioBackend.LogChangesToValue(nameof(AudioBackend));
|
||||
MemoryManagerMode = new ReactiveObject<MemoryManagerMode>();
|
||||
MemoryManagerMode.LogChangesToValue(nameof(MemoryManagerMode));
|
||||
DramSize = new ReactiveObject<MemoryConfiguration>();
|
||||
DramSize.LogChangesToValue(nameof(DramSize));
|
||||
IgnoreMissingServices = new ReactiveObject<bool>();
|
||||
IgnoreMissingServices.LogChangesToValue(nameof(IgnoreMissingServices));
|
||||
AudioVolume = new ReactiveObject<float>();
|
||||
AudioVolume.LogChangesToValue(nameof(AudioVolume));
|
||||
UseHypervisor = new ReactiveObject<bool>();
|
||||
UseHypervisor.LogChangesToValue(nameof(UseHypervisor));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hid configuration section
|
||||
/// </summary>
|
||||
public class HidSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable or disable keyboard support (Independent from controllers binding)
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableKeyboard { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable mouse support (Independent from controllers binding)
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableMouse { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hotkey Keyboard Bindings
|
||||
/// </summary>
|
||||
public ReactiveObject<KeyboardHotkeys> Hotkeys { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Input device configuration.
|
||||
/// NOTE: This ReactiveObject won't issue an event when the List has elements added or removed.
|
||||
/// TODO: Implement a ReactiveList class.
|
||||
/// </summary>
|
||||
public ReactiveObject<List<InputConfig>> InputConfig { get; private set; }
|
||||
|
||||
public HidSection()
|
||||
{
|
||||
EnableKeyboard = new ReactiveObject<bool>();
|
||||
EnableMouse = new ReactiveObject<bool>();
|
||||
Hotkeys = new ReactiveObject<KeyboardHotkeys>();
|
||||
InputConfig = new ReactiveObject<List<InputConfig>>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Graphics configuration section
|
||||
/// </summary>
|
||||
public class GraphicsSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether or not backend threading is enabled. The "Auto" setting will determine whether threading should be enabled at runtime.
|
||||
/// </summary>
|
||||
public ReactiveObject<BackendThreading> BackendThreading { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Max Anisotropy. Values range from 0 - 16. Set to -1 to let the game decide.
|
||||
/// </summary>
|
||||
public ReactiveObject<float> MaxAnisotropy { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Aspect Ratio applied to the renderer window.
|
||||
/// </summary>
|
||||
public ReactiveObject<AspectRatio> AspectRatio { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolution Scale. An integer scale applied to applicable render targets. Values 1-4, or -1 to use a custom floating point scale instead.
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ResScale { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Custom Resolution Scale. A custom floating point scale applied to applicable render targets. Only active when Resolution Scale is -1.
|
||||
/// </summary>
|
||||
public ReactiveObject<float> ResScaleCustom { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dumps shaders in this local directory
|
||||
/// </summary>
|
||||
public ReactiveObject<string> ShadersDumpPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Vertical Sync
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableVsync { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Shader cache
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableShaderCache { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables texture recompression
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableTextureRecompression { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Macro high-level emulation
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableMacroHLE { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables color space passthrough, if available.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableColorSpacePassthrough { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Graphics backend
|
||||
/// </summary>
|
||||
public ReactiveObject<GraphicsBackend> GraphicsBackend { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies anti-aliasing to the renderer.
|
||||
/// </summary>
|
||||
public ReactiveObject<AntiAliasing> AntiAliasing { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the framebuffer upscaling type.
|
||||
/// </summary>
|
||||
public ReactiveObject<ScalingFilter> ScalingFilter { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the framebuffer upscaling level.
|
||||
/// </summary>
|
||||
public ReactiveObject<int> ScalingFilterLevel { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Preferred GPU
|
||||
/// </summary>
|
||||
public ReactiveObject<string> PreferredGpu { get; private set; }
|
||||
|
||||
public GraphicsSection()
|
||||
{
|
||||
BackendThreading = new ReactiveObject<BackendThreading>();
|
||||
BackendThreading.LogChangesToValue(nameof(BackendThreading));
|
||||
ResScale = new ReactiveObject<int>();
|
||||
ResScale.LogChangesToValue(nameof(ResScale));
|
||||
ResScaleCustom = new ReactiveObject<float>();
|
||||
ResScaleCustom.LogChangesToValue(nameof(ResScaleCustom));
|
||||
MaxAnisotropy = new ReactiveObject<float>();
|
||||
MaxAnisotropy.LogChangesToValue(nameof(MaxAnisotropy));
|
||||
AspectRatio = new ReactiveObject<AspectRatio>();
|
||||
AspectRatio.LogChangesToValue(nameof(AspectRatio));
|
||||
ShadersDumpPath = new ReactiveObject<string>();
|
||||
EnableVsync = new ReactiveObject<bool>();
|
||||
EnableVsync.LogChangesToValue(nameof(EnableVsync));
|
||||
EnableShaderCache = new ReactiveObject<bool>();
|
||||
EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache));
|
||||
EnableTextureRecompression = new ReactiveObject<bool>();
|
||||
EnableTextureRecompression.LogChangesToValue(nameof(EnableTextureRecompression));
|
||||
GraphicsBackend = new ReactiveObject<GraphicsBackend>();
|
||||
GraphicsBackend.LogChangesToValue(nameof(GraphicsBackend));
|
||||
PreferredGpu = new ReactiveObject<string>();
|
||||
PreferredGpu.LogChangesToValue(nameof(PreferredGpu));
|
||||
EnableMacroHLE = new ReactiveObject<bool>();
|
||||
EnableMacroHLE.LogChangesToValue(nameof(EnableMacroHLE));
|
||||
EnableColorSpacePassthrough = new ReactiveObject<bool>();
|
||||
EnableColorSpacePassthrough.LogChangesToValue(nameof(EnableColorSpacePassthrough));
|
||||
AntiAliasing = new ReactiveObject<AntiAliasing>();
|
||||
AntiAliasing.LogChangesToValue(nameof(AntiAliasing));
|
||||
ScalingFilter = new ReactiveObject<ScalingFilter>();
|
||||
ScalingFilter.LogChangesToValue(nameof(ScalingFilter));
|
||||
ScalingFilterLevel = new ReactiveObject<int>();
|
||||
ScalingFilterLevel.LogChangesToValue(nameof(ScalingFilterLevel));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplayer configuration section
|
||||
/// </summary>
|
||||
public class MultiplayerSection
|
||||
{
|
||||
/// <summary>
|
||||
/// GUID for the network interface used by LAN (or 0 for default)
|
||||
/// </summary>
|
||||
public ReactiveObject<string> LanInterfaceId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplayer Mode
|
||||
/// </summary>
|
||||
public ReactiveObject<MultiplayerMode> Mode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disable P2P
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> DisableP2p { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// LDN PassPhrase
|
||||
/// </summary>
|
||||
public ReactiveObject<string> LdnPassphrase { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// LDN Server
|
||||
/// </summary>
|
||||
public ReactiveObject<string> LdnServer { get; private set; }
|
||||
|
||||
public MultiplayerSection()
|
||||
{
|
||||
LanInterfaceId = new ReactiveObject<string>();
|
||||
Mode = new ReactiveObject<MultiplayerMode>();
|
||||
Mode.LogChangesToValue(nameof(MultiplayerMode));
|
||||
DisableP2p = new ReactiveObject<bool>();
|
||||
DisableP2p.LogChangesToValue(nameof(DisableP2p));
|
||||
LdnPassphrase = new ReactiveObject<string>();
|
||||
LdnPassphrase.LogChangesToValue(nameof(LdnPassphrase));
|
||||
LdnServer = new ReactiveObject<string>();
|
||||
LdnServer.LogChangesToValue(nameof(LdnServer));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default configuration instance
|
||||
/// </summary>
|
||||
public static ConfigurationState Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The UI section
|
||||
/// </summary>
|
||||
public UISection UI { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Logger section
|
||||
/// </summary>
|
||||
public LoggerSection Logger { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The System section
|
||||
/// </summary>
|
||||
public SystemSection System { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Graphics section
|
||||
/// </summary>
|
||||
public GraphicsSection Graphics { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Hid section
|
||||
/// </summary>
|
||||
public HidSection Hid { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Multiplayer section
|
||||
/// </summary>
|
||||
public MultiplayerSection Multiplayer { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables Discord Rich Presence
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableDiscordIntegration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks for updates when Ryujinx starts when enabled
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> CheckUpdatesOnStart { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Show "Confirm Exit" Dialog
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowConfirmExit { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ignore Applet
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> IgnoreApplet { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables save window size, position and state on close.
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> RememberWindowState { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the redesigned title bar
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> ShowTitleBar { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables hardware-accelerated rendering for Avalonia
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> EnableHardwareAcceleration { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hide Cursor on Idle
|
||||
/// </summary>
|
||||
public ReactiveObject<HideCursorMode> HideCursor { get; private set; }
|
||||
|
||||
private ConfigurationState()
|
||||
{
|
||||
UI = new UISection();
|
||||
Logger = new LoggerSection();
|
||||
System = new SystemSection();
|
||||
Graphics = new GraphicsSection();
|
||||
Hid = new HidSection();
|
||||
Multiplayer = new MultiplayerSection();
|
||||
EnableDiscordIntegration = new ReactiveObject<bool>();
|
||||
CheckUpdatesOnStart = new ReactiveObject<bool>();
|
||||
ShowConfirmExit = new ReactiveObject<bool>();
|
||||
IgnoreApplet = new ReactiveObject<bool>();
|
||||
IgnoreApplet.LogChangesToValue(nameof(IgnoreApplet));
|
||||
RememberWindowState = new ReactiveObject<bool>();
|
||||
ShowTitleBar = new ReactiveObject<bool>();
|
||||
EnableHardwareAcceleration = new ReactiveObject<bool>();
|
||||
HideCursor = new ReactiveObject<HideCursorMode>();
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Logging.Targets;
|
||||
@ -11,103 +10,69 @@ namespace Ryujinx.UI.Common.Configuration
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
ConfigurationState.Instance.Logger.EnableDebug.Event += ReloadEnableDebug;
|
||||
ConfigurationState.Instance.Logger.EnableStub.Event += ReloadEnableStub;
|
||||
ConfigurationState.Instance.Logger.EnableInfo.Event += ReloadEnableInfo;
|
||||
ConfigurationState.Instance.Logger.EnableWarn.Event += ReloadEnableWarning;
|
||||
ConfigurationState.Instance.Logger.EnableError.Event += ReloadEnableError;
|
||||
ConfigurationState.Instance.Logger.EnableTrace.Event += ReloadEnableTrace;
|
||||
ConfigurationState.Instance.Logger.EnableGuest.Event += ReloadEnableGuest;
|
||||
ConfigurationState.Instance.Logger.EnableFsAccessLog.Event += ReloadEnableFsAccessLog;
|
||||
ConfigurationState.Instance.Logger.FilteredClasses.Event += ReloadFilteredClasses;
|
||||
ConfigurationState.Instance.Logger.EnableFileLog.Event += ReloadFileLogger;
|
||||
}
|
||||
|
||||
private static void ReloadEnableDebug(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Debug, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableStub(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Stub, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableInfo(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Info, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableWarning(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Warning, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableError(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Error, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableTrace(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Trace, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableGuest(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.Guest, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadEnableFsAccessLog(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Logger.SetEnable(LogLevel.AccessLog, e.NewValue);
|
||||
}
|
||||
|
||||
private static void ReloadFilteredClasses(object sender, ReactiveEventArgs<LogClass[]> e)
|
||||
{
|
||||
bool noFilter = e.NewValue.Length == 0;
|
||||
|
||||
foreach (var logClass in Enum.GetValues<LogClass>())
|
||||
ConfigurationState.Instance.Logger.EnableDebug.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Debug, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableStub.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Stub, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableInfo.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Info, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableWarn.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Warning, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableError.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Error, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableTrace.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Trace, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableGuest.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.Guest, e.NewValue);
|
||||
ConfigurationState.Instance.Logger.EnableFsAccessLog.Event +=
|
||||
(_, e) => Logger.SetEnable(LogLevel.AccessLog, e.NewValue);
|
||||
|
||||
ConfigurationState.Instance.Logger.FilteredClasses.Event += (_, e) =>
|
||||
{
|
||||
Logger.SetEnable(logClass, noFilter);
|
||||
}
|
||||
bool noFilter = e.NewValue.Length == 0;
|
||||
|
||||
foreach (var logClass in e.NewValue)
|
||||
{
|
||||
Logger.SetEnable(logClass, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReloadFileLogger(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
if (e.NewValue)
|
||||
{
|
||||
string logDir = AppDataManager.LogsDirPath;
|
||||
FileStream logFile = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(logDir))
|
||||
foreach (var logClass in Enum.GetValues<LogClass>())
|
||||
{
|
||||
logFile = FileLogTarget.PrepareLogFile(logDir);
|
||||
Logger.SetEnable(logClass, noFilter);
|
||||
}
|
||||
|
||||
if (logFile == null)
|
||||
foreach (var logClass in e.NewValue)
|
||||
{
|
||||
Logger.SetEnable(logClass, true);
|
||||
}
|
||||
};
|
||||
|
||||
ConfigurationState.Instance.Logger.EnableFileLog.Event += (_, e) =>
|
||||
{
|
||||
if (e.NewValue)
|
||||
{
|
||||
string logDir = AppDataManager.LogsDirPath;
|
||||
FileStream logFile = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(logDir))
|
||||
{
|
||||
logFile = FileLogTarget.PrepareLogFile(logDir);
|
||||
}
|
||||
|
||||
if (logFile == null)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application,
|
||||
"No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable.");
|
||||
Logger.RemoveTarget("file");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.AddTarget(new AsyncLogTargetWrapper(
|
||||
new FileLogTarget("file", logFile),
|
||||
1000
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, "No writable log directory available. Make sure either the Logs directory, Application Data, or the Ryujinx directory is writable.");
|
||||
Logger.RemoveTarget("file");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.AddTarget(new AsyncLogTargetWrapper(
|
||||
new FileLogTarget("file", logFile),
|
||||
1000,
|
||||
AsyncLogTargetOverflowAction.Block
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.RemoveTarget("file");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ namespace Ryujinx.UI.Common.Configuration.UI
|
||||
public bool AppColumn { get; set; }
|
||||
public bool DevColumn { get; set; }
|
||||
public bool VersionColumn { get; set; }
|
||||
public bool LdnInfoColumn { get; set; }
|
||||
public bool TimePlayedColumn { get; set; }
|
||||
public bool LastPlayedColumn { get; set; }
|
||||
public bool FileExtColumn { get; set; }
|
||||
|
@ -1,11 +1,10 @@
|
||||
using DiscordRPC;
|
||||
using Humanizer;
|
||||
using LibHac.Bcat;
|
||||
using Humanizer.Localisation;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.HLE.Loaders.Processes;
|
||||
using Ryujinx.UI.App.Common;
|
||||
using Ryujinx.UI.Common.Configuration;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@ -15,9 +14,13 @@ namespace Ryujinx.UI.Common
|
||||
{
|
||||
public static Timestamps StartedAt { get; set; }
|
||||
|
||||
private static readonly string _description = ReleaseInformation.IsValid
|
||||
? $"v{ReleaseInformation.Version} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}@{ReleaseInformation.BuildGitHash}"
|
||||
: "dev build";
|
||||
private static string VersionString
|
||||
=> (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}";
|
||||
|
||||
private static readonly string _description =
|
||||
ReleaseInformation.IsValid
|
||||
? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}"
|
||||
: "dev build";
|
||||
|
||||
private const string ApplicationId = "1293250299716173864";
|
||||
|
||||
@ -74,13 +77,13 @@ namespace Ryujinx.UI.Common
|
||||
Assets = new Assets
|
||||
{
|
||||
LargeImageKey = _discordGameAssetKeys.Contains(procRes.ProgramIdText) ? procRes.ProgramIdText : "game",
|
||||
LargeImageText = TruncateToByteLength($"{appMeta.Title} | {procRes.DisplayVersion}"),
|
||||
LargeImageText = TruncateToByteLength($"{appMeta.Title} (v{procRes.DisplayVersion})"),
|
||||
SmallImageKey = "ryujinx",
|
||||
SmallImageText = TruncateToByteLength(_description)
|
||||
},
|
||||
Details = TruncateToByteLength($"Playing {appMeta.Title}"),
|
||||
State = appMeta.LastPlayed.HasValue && appMeta.TimePlayed.TotalSeconds > 5
|
||||
? $"Total play time: {appMeta.TimePlayed.Humanize(2, false)}"
|
||||
? $"Total play time: {appMeta.TimePlayed.Humanize(2, false, maxUnit: TimeUnit.Hour)}"
|
||||
: "Never played",
|
||||
Timestamps = Timestamps.Now
|
||||
});
|
||||
@ -245,7 +248,7 @@ namespace Ryujinx.UI.Common
|
||||
"0100744001588000", // Cars 3: Driven to Win
|
||||
"0100b41013c82000", // Cruis'n Blast
|
||||
"01008c8012920000", // Dying Light Platinum Edition
|
||||
"01000a10041ea000", // The Elder Scrolls V: Skyrim
|
||||
"010073c01af34000", // LEGO Horizon Adventures
|
||||
"0100770008dd8000", // Monster Hunter Generations Ultimate
|
||||
"0100b04011742000", // Monster Hunter Rise
|
||||
"0100853015e86000", // No Man's Sky
|
||||
@ -260,6 +263,7 @@ namespace Ryujinx.UI.Common
|
||||
"0100d7a01b7a2000", // Star Wars: Bounty Hunter
|
||||
"0100800015926000", // Suika Game
|
||||
"0100e46006708000", // Terraria
|
||||
"01000a10041ea000", // The Elder Scrolls V: Skyrim
|
||||
"010080b00ad66000", // Undertale
|
||||
];
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ namespace Ryujinx.UI.Common.Helper
|
||||
public static string LaunchPathArg { get; private set; }
|
||||
public static string LaunchApplicationId { get; private set; }
|
||||
public static bool StartFullscreenArg { get; private set; }
|
||||
public static bool HideAvailableUpdates { get; private set; }
|
||||
|
||||
public static void ParseArguments(string[] args)
|
||||
{
|
||||
@ -93,6 +94,9 @@ namespace Ryujinx.UI.Common.Helper
|
||||
|
||||
OverrideHideCursor = args[++i];
|
||||
break;
|
||||
case "--hide-updates":
|
||||
HideAvailableUpdates = true;
|
||||
break;
|
||||
case "--software-gui":
|
||||
OverrideHardwareAcceleration = false;
|
||||
break;
|
||||
|
@ -207,6 +207,9 @@ namespace Ryujinx.Ava
|
||||
ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
|
||||
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
|
||||
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
|
||||
ConfigurationState.Instance.Multiplayer.LdnPassphrase.Event += UpdateLdnPassphraseState;
|
||||
ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState;
|
||||
ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState;
|
||||
|
||||
_gpuCancellationTokenSource = new CancellationTokenSource();
|
||||
_gpuDoneEvent = new ManualResetEvent(false);
|
||||
@ -491,6 +494,21 @@ namespace Ryujinx.Ava
|
||||
Device.Configuration.MultiplayerMode = e.NewValue;
|
||||
}
|
||||
|
||||
private void UpdateLdnPassphraseState(object sender, ReactiveEventArgs<string> e)
|
||||
{
|
||||
Device.Configuration.MultiplayerLdnPassphrase = e.NewValue;
|
||||
}
|
||||
|
||||
private void UpdateLdnServerState(object sender, ReactiveEventArgs<string> e)
|
||||
{
|
||||
Device.Configuration.MultiplayerLdnServer = e.NewValue;
|
||||
}
|
||||
|
||||
private void UpdateDisableP2pState(object sender, ReactiveEventArgs<bool> e)
|
||||
{
|
||||
Device.Configuration.MultiplayerDisableP2p = e.NewValue;
|
||||
}
|
||||
|
||||
public void ToggleVSync()
|
||||
{
|
||||
Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
|
||||
@ -863,10 +881,11 @@ namespace Ryujinx.Ava
|
||||
ConfigurationState.Instance.Graphics.AspectRatio,
|
||||
ConfigurationState.Instance.System.AudioVolume,
|
||||
ConfigurationState.Instance.System.UseHypervisor,
|
||||
ConfigurationState.Instance.Multiplayer.LanInterfaceId,
|
||||
ConfigurationState.Instance.Multiplayer.Mode
|
||||
)
|
||||
);
|
||||
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
|
||||
ConfigurationState.Instance.Multiplayer.Mode,
|
||||
ConfigurationState.Instance.Multiplayer.DisableP2p,
|
||||
ConfigurationState.Instance.Multiplayer.LdnPassphrase,
|
||||
ConfigurationState.Instance.Multiplayer.LdnServer));
|
||||
}
|
||||
|
||||
private static IHardwareDeviceDriver InitializeAudio()
|
||||
@ -1050,7 +1069,7 @@ namespace Ryujinx.Ava
|
||||
string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld];
|
||||
|
||||
UpdateShaderCount();
|
||||
|
||||
|
||||
if (GraphicsConfig.ResScale != 1)
|
||||
{
|
||||
dockedMode += $" ({GraphicsConfig.ResScale}x)";
|
||||
|
@ -456,7 +456,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "أنت غير متصل بالإنترنت.",
|
||||
"DialogUpdaterNoInternetSubMessage": "يرجى التحقق من أن لديك اتصال إنترنت فعال!",
|
||||
"DialogUpdaterDirtyBuildMessage": "لا يمكنك تحديث نسخة القذرة من ريوجينكس!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "الرجاء تحميل ريوجينكس من https://https://github.com/GreemDev/Ryujinx/releases إذا كنت تبحث عن إصدار مدعوم.",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "الرجاء تحميل ريوجينكس من https://ryujinx.app/download إذا كنت تبحث عن إصدار مدعوم.",
|
||||
"DialogRestartRequiredMessage": "يتطلب إعادة التشغيل",
|
||||
"DialogThemeRestartMessage": "تم حفظ السمة. إعادة التشغيل مطلوبة لتطبيق السمة.",
|
||||
"DialogThemeRestartSubMessage": "هل تريد إعادة التشغيل",
|
||||
@ -728,6 +728,8 @@
|
||||
"DlcWindowTitle": "إدارة المحتوى القابل للتنزيل لـ {0} ({1})",
|
||||
"ModWindowTitle": "إدارة التعديلات لـ {0} ({1})",
|
||||
"UpdateWindowTitle": "مدير تحديث العنوان",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||
"CheatWindowHeading": "الغش متوفر لـ {0} [{1}]",
|
||||
|
@ -456,7 +456,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "Es besteht keine Verbindung mit dem Internet!",
|
||||
"DialogUpdaterNoInternetSubMessage": "Bitte vergewissern, dass eine funktionierende Internetverbindung existiert!",
|
||||
"DialogUpdaterDirtyBuildMessage": "Inoffizielle Versionen von Ryujinx können nicht aktualisiert werden",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Lade Ryujinx bitte von hier herunter, um eine unterstützte Version zu erhalten: https://https://github.com/GreemDev/Ryujinx/releases/",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Lade Ryujinx bitte von hier herunter, um eine unterstützte Version zu erhalten: https://ryujinx.app/download",
|
||||
"DialogRestartRequiredMessage": "Neustart erforderlich",
|
||||
"DialogThemeRestartMessage": "Das Design wurde gespeichert. Ein Neustart ist erforderlich, um das Design anzuwenden.",
|
||||
"DialogThemeRestartSubMessage": "Jetzt neu starten?",
|
||||
@ -728,6 +728,8 @@
|
||||
"DlcWindowTitle": "Spiel-DLC verwalten",
|
||||
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
||||
"UpdateWindowTitle": "Spiel-Updates verwalten",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||
"CheatWindowHeading": "Cheats verfügbar für {0} [{1}]",
|
||||
|
@ -456,7 +456,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "Δεν είστε συνδεδεμένοι στο Διαδίκτυο!",
|
||||
"DialogUpdaterNoInternetSubMessage": "Επαληθεύστε ότι έχετε σύνδεση στο Διαδίκτυο που λειτουργεί!",
|
||||
"DialogUpdaterDirtyBuildMessage": "Δεν μπορείτε να ενημερώσετε μία Πρόχειρη Έκδοση του Ryujinx!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Κάντε λήψη του Ryujinx στη διεύθυνση https://https://github.com/GreemDev/Ryujinx/releases/ εάν αναζητάτε μία υποστηριζόμενη έκδοση.",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Κάντε λήψη του Ryujinx στη διεύθυνση https://ryujinx.app/download εάν αναζητάτε μία υποστηριζόμενη έκδοση.",
|
||||
"DialogRestartRequiredMessage": "Απαιτείται Επανεκκίνηση",
|
||||
"DialogThemeRestartMessage": "Το θέμα έχει αποθηκευτεί. Απαιτείται επανεκκίνηση για την εφαρμογή του θέματος.",
|
||||
"DialogThemeRestartSubMessage": "Θέλετε να κάνετε επανεκκίνηση",
|
||||
@ -728,6 +728,8 @@
|
||||
"DlcWindowTitle": "Downloadable Content Manager",
|
||||
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
||||
"UpdateWindowTitle": "Διαχειριστής Ενημερώσεων Τίτλου",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||
"CheatWindowHeading": "Διαθέσιμα Cheats για {0} [{1}]",
|
||||
|
@ -462,7 +462,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "You are not connected to the Internet!",
|
||||
"DialogUpdaterNoInternetSubMessage": "Please verify that you have a working Internet connection!",
|
||||
"DialogUpdaterDirtyBuildMessage": "You cannot update a Dirty build of Ryujinx!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Please download Ryujinx at https://github.com/GreemDev/Ryujinx/releases/ if you are looking for a supported version.",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Please download Ryujinx at https://ryujinx.app/download if you are looking for a supported version.",
|
||||
"DialogRestartRequiredMessage": "Restart Required",
|
||||
"DialogThemeRestartMessage": "Theme has been saved. A restart is needed to apply the theme.",
|
||||
"DialogThemeRestartSubMessage": "Do you want to restart",
|
||||
@ -767,6 +767,8 @@
|
||||
"XCITrimmerDeselectDisplayed": "Deselect Shown",
|
||||
"XCITrimmerSortName": "Title",
|
||||
"XCITrimmerSortSaved": "Space Savings",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
|
||||
@ -848,5 +850,17 @@
|
||||
"MultiplayerMode": "Mode:",
|
||||
"MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.",
|
||||
"MultiplayerModeDisabled": "Disabled",
|
||||
"MultiplayerModeLdnMitm": "ldn_mitm"
|
||||
"MultiplayerModeLdnMitm": "ldn_mitm",
|
||||
"MultiplayerModeLdnRyu": "RyuLDN",
|
||||
"MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)",
|
||||
"MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.",
|
||||
"LdnPassphrase": "Network Passphrase:",
|
||||
"LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.",
|
||||
"LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.",
|
||||
"LdnPassphraseInputPublic": "(public)",
|
||||
"GenLdnPass": "Generate Random",
|
||||
"GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.",
|
||||
"ClearLdnPass": "Clear",
|
||||
"ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.",
|
||||
"InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\""
|
||||
}
|
||||
|
@ -10,10 +10,10 @@
|
||||
"SettingsTabSystemUseHypervisor": "Usar hipervisor",
|
||||
"MenuBarFile": "_Archivo",
|
||||
"MenuBarFileOpenFromFile": "_Cargar aplicación desde un archivo",
|
||||
"MenuBarFileOpenFromFileError": "No applications found in selected file.",
|
||||
"MenuBarFileOpenFromFileError": "No se encontraron aplicaciones en el archivo seleccionado.",
|
||||
"MenuBarFileOpenUnpacked": "Cargar juego _desempaquetado",
|
||||
"MenuBarFileLoadDlcFromFolder": "Load DLC From Folder",
|
||||
"MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder",
|
||||
"MenuBarFileLoadDlcFromFolder": "Cargar DLC Desde Carpeta",
|
||||
"MenuBarFileLoadTitleUpdatesFromFolder": "Cargar Actualizaciones de Títulos Desde Carpeta",
|
||||
"MenuBarFileOpenEmuFolder": "Abrir carpeta de Ryujinx",
|
||||
"MenuBarFileOpenLogsFolder": "Abrir carpeta de registros",
|
||||
"MenuBarFileExit": "_Salir",
|
||||
@ -34,7 +34,7 @@
|
||||
"MenuBarToolsInstallFileTypes": "Instalar tipos de archivo",
|
||||
"MenuBarToolsUninstallFileTypes": "Desinstalar tipos de archivo",
|
||||
"MenuBarView": "_View",
|
||||
"MenuBarViewWindow": "Window Size",
|
||||
"MenuBarViewWindow": "Tamaño Ventana",
|
||||
"MenuBarViewWindow720": "720p",
|
||||
"MenuBarViewWindow1080": "1080p",
|
||||
"MenuBarHelp": "_Ayuda",
|
||||
@ -99,15 +99,15 @@
|
||||
"SettingsTabGeneralEnableDiscordRichPresence": "Habilitar estado en Discord",
|
||||
"SettingsTabGeneralCheckUpdatesOnLaunch": "Buscar actualizaciones al iniciar",
|
||||
"SettingsTabGeneralShowConfirmExitDialog": "Mostrar diálogo de confirmación al cerrar",
|
||||
"SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
|
||||
"SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)",
|
||||
"SettingsTabGeneralRememberWindowState": "Recordar Tamaño/Posición de la Ventana",
|
||||
"SettingsTabGeneralShowTitleBar": "Mostrar Barra de Título (Requiere reinicio)",
|
||||
"SettingsTabGeneralHideCursor": "Esconder el cursor:",
|
||||
"SettingsTabGeneralHideCursorNever": "Nunca",
|
||||
"SettingsTabGeneralHideCursorOnIdle": "Ocultar cursor cuando esté inactivo",
|
||||
"SettingsTabGeneralHideCursorAlways": "Siempre",
|
||||
"SettingsTabGeneralGameDirectories": "Carpetas de juegos",
|
||||
"SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories",
|
||||
"SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically",
|
||||
"SettingsTabGeneralAutoloadDirectories": "Carpetas de DLC/Actualizaciones para Carga Automática",
|
||||
"SettingsTabGeneralAutoloadNote": "DLC y Actualizaciones que hacen referencia a archivos ausentes serán desactivado automáticamente",
|
||||
"SettingsTabGeneralAdd": "Agregar",
|
||||
"SettingsTabGeneralRemove": "Quitar",
|
||||
"SettingsTabSystem": "Sistema",
|
||||
@ -142,10 +142,10 @@
|
||||
"SettingsTabSystemSystemTime": "Hora del sistema:",
|
||||
"SettingsTabSystemEnableVsync": "Sincronización vertical",
|
||||
"SettingsTabSystemEnablePptc": "PPTC (Cache de Traducción de Perfil Persistente)",
|
||||
"SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC",
|
||||
"SettingsTabSystemEnableLowPowerPptc": "Cache PPTC de bajo consumo",
|
||||
"SettingsTabSystemEnableFsIntegrityChecks": "Comprobar integridad de los archivos",
|
||||
"SettingsTabSystemAudioBackend": "Motor de audio:",
|
||||
"SettingsTabSystemAudioBackendDummy": "Vacio",
|
||||
"SettingsTabSystemAudioBackendDummy": "Vacío",
|
||||
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
|
||||
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
|
||||
"SettingsTabSystemAudioBackendSDL2": "SDL2",
|
||||
@ -407,7 +407,7 @@
|
||||
"AvatarSetBackgroundColor": "Establecer color de fondo",
|
||||
"AvatarClose": "Cerrar",
|
||||
"ControllerSettingsLoadProfileToolTip": "Cargar perfil",
|
||||
"ControllerSettingsViewProfileToolTip": "View Profile",
|
||||
"ControllerSettingsViewProfileToolTip": "Ver perfil",
|
||||
"ControllerSettingsAddProfileToolTip": "Agregar perfil",
|
||||
"ControllerSettingsRemoveProfileToolTip": "Eliminar perfil",
|
||||
"ControllerSettingsSaveProfileToolTip": "Guardar perfil",
|
||||
@ -456,12 +456,12 @@
|
||||
"DialogUpdaterNoInternetMessage": "¡No estás conectado a internet!",
|
||||
"DialogUpdaterNoInternetSubMessage": "¡Por favor, verifica que tu conexión a Internet funciona!",
|
||||
"DialogUpdaterDirtyBuildMessage": "¡No puedes actualizar una versión \"dirty\" de Ryujinx!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Por favor, descarga Ryujinx en https://https://github.com/GreemDev/Ryujinx/releases/ si buscas una versión con soporte.",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Por favor, descarga Ryujinx en https://ryujinx.app/download si buscas una versión con soporte.",
|
||||
"DialogRestartRequiredMessage": "Se necesita reiniciar",
|
||||
"DialogThemeRestartMessage": "Tema guardado. Se necesita reiniciar para aplicar el tema.",
|
||||
"DialogThemeRestartSubMessage": "¿Quieres reiniciar?",
|
||||
"DialogFirmwareInstallEmbeddedMessage": "¿Quieres instalar el firmware incluido en este juego? (Firmware versión {0})",
|
||||
"DialogFirmwareInstallEmbeddedSuccessMessage": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\nThe emulator will now start.",
|
||||
"DialogFirmwareInstallEmbeddedSuccessMessage": "No se encontró ning{un firmware instalado pero Ryujinx pudo instalar firmware {0} del juego proporcionado.\nEl emulador iniciará.",
|
||||
"DialogFirmwareNoFirmwareInstalledMessage": "No hay firmware instalado",
|
||||
"DialogFirmwareInstalledMessage": "Se instaló el firmware {0}",
|
||||
"DialogInstallFileTypesSuccessMessage": "¡Tipos de archivos instalados con éxito!",
|
||||
@ -568,22 +568,22 @@
|
||||
"AddGameDirBoxTooltip": "Elige un directorio de juegos para mostrar en la ventana principal",
|
||||
"AddGameDirTooltip": "Agrega un directorio de juegos a la lista",
|
||||
"RemoveGameDirTooltip": "Quita el directorio seleccionado de la lista",
|
||||
"AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list",
|
||||
"AddAutoloadDirTooltip": "Add an autoload directory to the list",
|
||||
"RemoveAutoloadDirTooltip": "Remove selected autoload directory",
|
||||
"AddAutoloadDirBoxTooltip": "Elige un directorio de carga automática para agregar a la lista",
|
||||
"AddAutoloadDirTooltip": "Agregar un directorio de carga automática a la lista",
|
||||
"RemoveAutoloadDirTooltip": "Eliminar el directorio de carga automática seleccionado",
|
||||
"CustomThemeCheckTooltip": "Activa o desactiva los temas personalizados para la interfaz",
|
||||
"CustomThemePathTooltip": "Carpeta que contiene los temas personalizados para la interfaz",
|
||||
"CustomThemeBrowseTooltip": "Busca un tema personalizado para la interfaz",
|
||||
"DockModeToggleTooltip": "El modo dock o modo TV hace que la consola emulada se comporte como una Nintendo Switch en su dock. Esto mejora la calidad gráfica en la mayoría de los juegos. Del mismo modo, si lo desactivas, el sistema emulado se comportará como una Nintendo Switch en modo portátil, reduciendo la cálidad de los gráficos.\n\nConfigura los controles de \"Jugador\" 1 si planeas jugar en modo dock/TV; configura los controles de \"Portátil\" si planeas jugar en modo portátil.\n\nActívalo si no sabes qué hacer.",
|
||||
"DirectKeyboardTooltip": "Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.\n\nOnly works with games that natively support keyboard usage on Switch hardware.\n\nLeave OFF if unsure.",
|
||||
"DirectMouseTooltip": "Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.\n\nOnly works with games that natively support mouse controls on Switch hardware, which are few and far between.\n\nWhen enabled, touch screen functionality may not work.\n\nLeave OFF if unsure.",
|
||||
"DirectKeyboardTooltip": "Soporte de acceso directo al teclado (HID). Proporciona a los juegos acceso a su teclado como dispositivo de entrada de texto.\n\nSolo funciona con juegos que permiten de forma nativa el uso del teclado en el hardware de Switch.\n\nDesactívalo si no sabes qué hacer.",
|
||||
"DirectMouseTooltip": "Soporte de acceso directo al mouse (HID). Proporciona a los juegos acceso a su mouse como puntero.\n\nSolo funciona con juegos que permiten de forma nativa el uso de controles con mouse en el hardware de switch, lo cual son pocos.\n\nCuando esté activado, la funcionalidad de pantalla táctil puede no funcionar.\n\nDesactívalo si no sabes qué hacer.",
|
||||
"RegionTooltip": "Cambia la región del sistema",
|
||||
"LanguageTooltip": "Cambia el idioma del sistema",
|
||||
"TimezoneTooltip": "Cambia la zona horaria del sistema",
|
||||
"TimeTooltip": "Cambia la hora del sistema",
|
||||
"VSyncToggleTooltip": "Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck.\n\nCan be toggled in-game with a hotkey of your preference (F1 by default). We recommend doing this if you plan on disabling it.\n\nLeave ON if unsure.",
|
||||
"VSyncToggleTooltip": "Sincronización vertical de la consola emulada. En práctica un limitador del framerate para la mayoría de los juegos; desactivando puede causar que juegos corran a mayor velocidad o que las pantallas de carga tarden más o queden atascados.\n\nSe puede alternar en juego utilizando una tecla de acceso rápido configurable (F1 by default). Recomendamos hacer esto en caso de querer desactivar sincroniziación vertical.\n\nDesactívalo si no sabes qué hacer.",
|
||||
"PptcToggleTooltip": "Guarda funciones de JIT traducidas para que no sea necesario traducirlas cada vez que el juego carga.\n\nReduce los tirones y acelera significativamente el tiempo de inicio de los juegos después de haberlos ejecutado al menos una vez.\n\nActívalo si no sabes qué hacer.",
|
||||
"LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.",
|
||||
"LowPowerPptcToggleTooltip": "Cargue el PPTC utilizando un tercio de la cantidad de núcleos.",
|
||||
"FsIntegrityToggleTooltip": "Comprueba si hay archivos corruptos en los juegos que ejecutes al abrirlos, y si detecta archivos corruptos, muestra un error de Hash en los registros.\n\nEsto no tiene impacto alguno en el rendimiento y está pensado para ayudar a resolver problemas.\n\nActívalo si no sabes qué hacer.",
|
||||
"AudioBackendTooltip": "Cambia el motor usado para renderizar audio.\n\nSDL2 es el preferido, mientras que OpenAL y SoundIO se usan si hay problemas con este. Dummy no produce audio.\n\nSelecciona SDL2 si no sabes qué hacer.",
|
||||
"MemoryManagerTooltip": "Cambia la forma de mapear y acceder a la memoria del guest. Afecta en gran medida al rendimiento de la CPU emulada.\n\nSelecciona \"Host sin verificación\" si no sabes qué hacer.",
|
||||
@ -597,10 +597,10 @@
|
||||
"GraphicsBackendThreadingTooltip": "Ejecuta los comandos del motor gráfico en un segundo hilo. Acelera la compilación de sombreadores, reduce los tirones, y mejora el rendimiento en controladores gráficos que no realicen su propio procesamiento con múltiples hilos. Rendimiento ligeramente superior en controladores gráficos que soporten múltiples hilos.\n\nSelecciona \"Auto\" si no sabes qué hacer.",
|
||||
"GalThreadingTooltip": "Ejecuta los comandos del motor gráfico en un segundo hilo. Acelera la compilación de sombreadores, reduce los tirones, y mejora el rendimiento en controladores gráficos que no realicen su propio procesamiento con múltiples hilos. Rendimiento ligeramente superior en controladores gráficos que soporten múltiples hilos.\n\nSelecciona \"Auto\" si no sabes qué hacer.",
|
||||
"ShaderCacheToggleTooltip": "Guarda una caché de sombreadores en disco, la cual reduce los tirones a medida que vas jugando.\n\nActívalo si no sabes qué hacer.",
|
||||
"ResolutionScaleTooltip": "Multiplies the game's rendering resolution.\n\nA few games may not work with this and look pixelated even when the resolution is increased; for those games, you may need to find mods that remove anti-aliasing or that increase their internal rendering resolution. For using the latter, you'll likely want to select Native.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nKeep in mind 4x is overkill for virtually any setup.",
|
||||
"ResolutionScaleTooltip": "Multiplica la resolución de rendereo del juego.\n\nAlgunos juegos podrían no funcionar con esto y verse pixelado al aumentar la resolución; en esos casos, quizás sería necesario buscar mods que de anti-aliasing o que aumenten la resolución interna. Para usar este último, probablemente necesitarás seleccionar Nativa.\n\nEsta opción puede ser modificada mientras que un juego este corriendo haciendo click en \"Aplicar\" más abajo; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nTener en cuenta que 4x es excesivo para prácticamente cualquier configuración.",
|
||||
"ResolutionScaleEntryTooltip": "Escalado de resolución de coma flotante, como por ejemplo 1,5. Los valores no íntegros pueden causar errores gráficos o crashes.",
|
||||
"AnisotropyTooltip": "Level of Anisotropic Filtering. Set to Auto to use the value requested by the game.",
|
||||
"AspectRatioTooltip": "Aspect Ratio applied to the renderer window.\n\nOnly change this if you're using an aspect ratio mod for your game, otherwise the graphics will be stretched.\n\nLeave on 16:9 if unsure.",
|
||||
"AnisotropyTooltip": "Nivel de filtrado anisotrópico. Setear en Auto para utilizar el valor solicitado por el juego.",
|
||||
"AspectRatioTooltip": "Relación de aspecto aplicada a la ventana del renderizador.\n\nSolamente modificar esto si estás utilizando un mod de relación de aspecto para su juego, en cualquier otro caso los gráficos se estirarán.\n\nDejar en 16:9 si no sabe que hacer.",
|
||||
"ShaderDumpPathTooltip": "Directorio en el cual se volcarán los sombreadores de los gráficos",
|
||||
"FileLogTooltip": "Guarda los registros de la consola en archivos en disco. No afectan al rendimiento.",
|
||||
"StubLogTooltip": "Escribe mensajes de Stub en la consola. No afectan al rendimiento.",
|
||||
@ -616,8 +616,8 @@
|
||||
"DebugLogTooltip": "Escribe mensajes de debug en la consola\n\nActiva esto solo si un miembro del equipo te lo pide expresamente, pues hará que el registro sea difícil de leer y empeorará el rendimiento del emulador.",
|
||||
"LoadApplicationFileTooltip": "Abre el explorador de archivos para elegir un archivo compatible con Switch para cargar",
|
||||
"LoadApplicationFolderTooltip": "Abre el explorador de archivos para elegir un archivo desempaquetado y compatible con Switch para cargar",
|
||||
"LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from",
|
||||
"LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from",
|
||||
"LoadDlcFromFolderTooltip": "Abrir un explorador de archivos para seleccionar una o más carpetas para cargar DLC de forma masiva",
|
||||
"LoadTitleUpdatesFromFolderTooltip": "Abrir un explorador de archivos para seleccionar una o más carpetas para cargar actualizaciones de título de forma masiva",
|
||||
"OpenRyujinxFolderTooltip": "Abre la carpeta de sistema de Ryujinx",
|
||||
"OpenRyujinxLogsTooltip": "Abre la carpeta en la que se guardan los registros",
|
||||
"ExitTooltip": "Cierra Ryujinx",
|
||||
@ -726,18 +726,20 @@
|
||||
"UserProfileWindowTitle": "Administrar perfiles de usuario",
|
||||
"CheatWindowTitle": "Administrar cheats",
|
||||
"DlcWindowTitle": "Administrar contenido descargable",
|
||||
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
||||
"ModWindowTitle": "Administrar Mods para {0} ({1})",
|
||||
"UpdateWindowTitle": "Administrar actualizaciones",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} nueva(s) actualización(es) agregada(s)",
|
||||
"UpdateWindowBundledContentNotice": "Las actualizaciones agrupadas no pueden ser eliminadas, solamente deshabilitadas.",
|
||||
"CheatWindowHeading": "Cheats disponibles para {0} [{1}]",
|
||||
"BuildId": "Id de compilación:",
|
||||
"DlcWindowHeading": "Contenido descargable disponible para {0} [{1}]",
|
||||
"DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added",
|
||||
"AutoloadDlcAddedMessage": "{0} new downloadable content(s) added",
|
||||
"AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed",
|
||||
"AutoloadUpdateAddedMessage": "{0} new update(s) added",
|
||||
"AutoloadUpdateRemovedMessage": "{0} missing update(s) removed",
|
||||
"DlcWindowDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)",
|
||||
"AutoloadDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)",
|
||||
"AutoloadDlcRemovedMessage": "Se eliminaron {0} contenido(s) descargable(s) faltantes",
|
||||
"AutoloadUpdateAddedMessage": "Se agregaron {0} nueva(s) actualización(es)",
|
||||
"AutoloadUpdateRemovedMessage": "Se eliminaron {0} actualización(es) faltantes",
|
||||
"ModWindowHeading": "{0} Mod(s)",
|
||||
"UserProfilesEditProfile": "Editar selección",
|
||||
"Cancel": "Cancelar",
|
||||
@ -753,9 +755,9 @@
|
||||
"UserProfilesName": "Nombre:",
|
||||
"UserProfilesUserId": "Id de Usuario:",
|
||||
"SettingsTabGraphicsBackend": "Fondo de gráficos",
|
||||
"SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.",
|
||||
"SettingsTabGraphicsBackendTooltip": "Seleccione el backend gráfico que utilizará el emulador.\n\nVulkan, en general, es mejor para todas las tarjetas gráficas modernas, mientras que sus controladores estén actualizados. Vulkan también cuenta con complicación más rápida de sombreadores (menos tirones) en todos los proveredores de GPU.\n\nOpenGL puede lograr mejores resultados en GPU Nvidia antiguas, GPU AMD antiguas en Linux o en GPUs con menor VRAM, aunque tirones de compilación de sombreadores serán mayores.\n\nSetear en Vulkan si no sabe que hacer. Setear en OpenGL si su GPU no tiene soporte para Vulkan aún con los últimos controladores gráficos.",
|
||||
"SettingsEnableTextureRecompression": "Activar recompresión de texturas",
|
||||
"SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.",
|
||||
"SettingsEnableTextureRecompressionTooltip": "Comprimir texturas ASTC para reducir uso de VRAM.\n\nJuegos que utilizan este formato de textura incluyen Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder y The Legend of Zelda: Tears of the Kingdom.\n\nTarjetas gráficas con 4GiB de VRAM o menos probalemente se caeran en algún momento mientras que estén corriendo estos juegos.\n\nActivar solo si está quedan sin VRAM en los juegos antes mencionados. Desactívalo si no sabes qué hacer.",
|
||||
"SettingsTabGraphicsPreferredGpu": "GPU preferida",
|
||||
"SettingsTabGraphicsPreferredGpuTooltip": "Selecciona la tarjeta gráfica que se utilizará con los back-end de gráficos Vulkan.\n\nNo afecta la GPU que utilizará OpenGL.\n\nFije a la GPU marcada como \"dGUP\" ante dudas. Si no hay una, no haga modificaciones.",
|
||||
"SettingsAppRequiredRestartMessage": "Reinicio de Ryujinx requerido.",
|
||||
@ -772,7 +774,7 @@
|
||||
"UserProfilesManageSaves": "Administrar mis partidas guardadas",
|
||||
"DeleteUserSave": "¿Quieres borrar los datos de usuario de este juego?",
|
||||
"IrreversibleActionNote": "Esta acción no es reversible.",
|
||||
"SaveManagerHeading": "Manage Saves for {0}",
|
||||
"SaveManagerHeading": "Administrar partidas guardadas para {0}",
|
||||
"SaveManagerTitle": "Administrador de datos de guardado.",
|
||||
"Name": "Nombre",
|
||||
"Size": "Tamaño",
|
||||
@ -781,11 +783,11 @@
|
||||
"Recover": "Recuperar",
|
||||
"UserProfilesRecoverHeading": "Datos de guardado fueron encontrados para las siguientes cuentas",
|
||||
"UserProfilesRecoverEmptyList": "No hay perfiles a recuperar",
|
||||
"GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.",
|
||||
"GraphicsAATooltip": "Aplica antia-aliasing al rendereo del juego.\n\nFXAA desenfocará la mayor parte del la iamgen, mientras que SMAA intentará encontrar bordes irregulares y suavizarlos.\n\nNo se recomienda usar en conjunto con filtro de escala FSR.\n\nEsta opción puede ser modificada mientras que esté corriendo el juego haciendo click en \"Aplicar\" más abajo; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nDejar en NADA si no está seguro.",
|
||||
"GraphicsAALabel": "Suavizado de bordes:",
|
||||
"GraphicsScalingFilterLabel": "Filtro de escalado:",
|
||||
"GraphicsScalingFilterTooltip": "Elija el filtro de escala que se aplicará al utilizar la escala de resolución.\n\nBilinear funciona bien para juegos 3D y es una opción predeterminada segura.\n\nSe recomienda el bilinear para juegos de pixel art.\n\nFSR 1.0 es simplemente un filtro de afilado, no se recomienda su uso con FXAA o SMAA.\n\nEsta opción se puede cambiar mientras se ejecuta un juego haciendo clic en \"Aplicar\" a continuación; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nDéjelo en BILINEAR si no está seguro.",
|
||||
"GraphicsScalingFilterBilinear": "Bilinear\n",
|
||||
"GraphicsScalingFilterBilinear": "Bilinear",
|
||||
"GraphicsScalingFilterNearest": "Cercano",
|
||||
"GraphicsScalingFilterFsr": "FSR",
|
||||
"GraphicsScalingFilterArea": "Area",
|
||||
@ -806,6 +808,18 @@
|
||||
"SettingsTabNetworkMultiplayer": "Multijugador",
|
||||
"MultiplayerMode": "Modo:",
|
||||
"MultiplayerModeTooltip": "Cambiar modo LDN multijugador.\n\nLdnMitm modificará la funcionalidad local de juego inalámbrico para funcionar como si fuera LAN, permitiendo locales conexiones de la misma red con otras instancias de Ryujinx y consolas hackeadas de Nintendo Switch que tienen instalado el módulo ldn_mitm.\n\nMultijugador requiere que todos los jugadores estén en la misma versión del juego (por ejemplo, Super Smash Bros. Ultimate v13.0.1 no se puede conectar a v13.0.0).\n\nDejar DESACTIVADO si no está seguro.",
|
||||
"MultiplayerModeDisabled": "Deshabilitar",
|
||||
"MultiplayerModeLdnMitm": "ldn_mitm"
|
||||
"MultiplayerModeDisabled": "Deshabilitado",
|
||||
"MultiplayerModeLdnMitm": "ldn_mitm",
|
||||
"MultiplayerModeLdnRyu": "RyuLDN",
|
||||
"MultiplayerDisableP2P": "Desactivar El Hosteo De Red P2P (puede aumentar latencia)",
|
||||
"MultiplayerDisableP2PTooltip": "Desactivar el hosteo de red P2P, pares se conectarán a través del servidor maestro en lugar de conectarse directamente contigo.",
|
||||
"LdnPassphrase": "Frase de contraseña de la Red:",
|
||||
"LdnPassphraseTooltip": "Solo podrás ver los juegos hosteados con la misma frase de contraseña que tú.",
|
||||
"LdnPassphraseInputTooltip": "Ingresar una frase de contraseña en formato Ryujinx-<8 caracteres hexadecimales>. Solamente podrás ver juegos hosteados con la misma frase de contraseña que tú.",
|
||||
"LdnPassphraseInputPublic": "(público)",
|
||||
"GenLdnPass": "Generar aleatorio",
|
||||
"GenLdnPassTooltip": "Genera una nueva frase de contraseña, que puede ser compartida con otros jugadores.",
|
||||
"ClearLdnPass": "Borrar",
|
||||
"ClearLdnPassTooltip": "Borra la frase de contraseña actual, regresando a la red pública.",
|
||||
"InvalidLdnPassphrase": "Frase de Contraseña Inválida! Debe ser en formato \"Ryujinx-<8 caracteres hexadecimales>\""
|
||||
}
|
||||
|
@ -456,7 +456,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "Vous n'êtes pas connecté à Internet !",
|
||||
"DialogUpdaterNoInternetSubMessage": "Veuillez vérifier que vous disposez d'une connexion Internet fonctionnelle !",
|
||||
"DialogUpdaterDirtyBuildMessage": "Vous ne pouvez pas mettre à jour une version Dirty de Ryujinx !",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Veuillez télécharger Ryujinx sur https://github.com/GreemDev/Ryujinx/releases/ si vous recherchez une version prise en charge.",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Veuillez télécharger Ryujinx sur https://ryujinx.app/download si vous recherchez une version prise en charge.",
|
||||
"DialogRestartRequiredMessage": "Redémarrage requis",
|
||||
"DialogThemeRestartMessage": "Le thème a été enregistré. Un redémarrage est requis pour appliquer le thème.",
|
||||
"DialogThemeRestartSubMessage": "Voulez-vous redémarrer",
|
||||
|
@ -456,7 +456,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "אתם לא מחוברים לאינטרנט!",
|
||||
"DialogUpdaterNoInternetSubMessage": "אנא ודא שיש לך חיבור אינטרנט תקין!",
|
||||
"DialogUpdaterDirtyBuildMessage": "אתם לא יכולים לעדכן מבנה מלוכלך של ריוג'ינקס!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "אם אתם מחפשים גרסא נתמכת, אנא הורידו את ריוג'ינקס בכתובת https://https://github.com/GreemDev/Ryujinx/releases",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "אם אתם מחפשים גרסא נתמכת, אנא הורידו את ריוג'ינקס בכתובת https://ryujinx.app/download",
|
||||
"DialogRestartRequiredMessage": "אתחול נדרש",
|
||||
"DialogThemeRestartMessage": "ערכת הנושא נשמרה. יש צורך בהפעלה מחדש כדי להחיל את ערכת הנושא.",
|
||||
"DialogThemeRestartSubMessage": "האם ברצונך להפעיל מחדש?",
|
||||
@ -728,6 +728,8 @@
|
||||
"DlcWindowTitle": "נהל הרחבות משחק עבור {0} ({1})",
|
||||
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
||||
"UpdateWindowTitle": "נהל עדכוני משחקים",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||
"CheatWindowHeading": "צ'יטים זמינים עבור {0} [{1}]",
|
||||
|
@ -33,8 +33,9 @@
|
||||
"MenuBarFileLoadDlcFromFolder": "Carica DLC Da una Cartella",
|
||||
"MenuBarFileLoadTitleUpdatesFromFolder": "Carica Aggiornamenti Da una Cartella",
|
||||
"MenuBarFileOpenFromFileError": "Nessuna applicazione trovata nel file selezionato",
|
||||
"MenuBarView": "_View",
|
||||
"MenuBarViewWindow": "Window Size",
|
||||
"MenuBarToolsXCITrimmer": "Trim XCI Files",
|
||||
"MenuBarView": "_Vista",
|
||||
"MenuBarViewWindow": "Dimensione Finestra",
|
||||
"MenuBarViewWindow720": "720p",
|
||||
"MenuBarViewWindow1080": "1080p",
|
||||
"MenuBarHelp": "_Aiuto",
|
||||
@ -84,8 +85,11 @@
|
||||
"GameListContextMenuOpenModsDirectoryToolTip": "Apre la cartella che contiene le mod dell'applicazione",
|
||||
"GameListContextMenuOpenSdModsDirectory": "Apri la cartella delle mod Atmosphere",
|
||||
"GameListContextMenuOpenSdModsDirectoryToolTip": "Apre la cartella alternativa di Atmosphere sulla scheda SD che contiene le mod dell'applicazione. Utile per le mod create per funzionare sull'hardware reale.",
|
||||
"StatusBarGamesLoaded": "{0}/{1} giochi caricati",
|
||||
"GameListContextMenuTrimXCI": "Controlla e Trimma i file XCI",
|
||||
"GameListContextMenuTrimXCIToolTip": "Controlla e Trimma i file XCI da Salvare Sullo Spazio del Disco",
|
||||
"StatusBarGamesLoaded": "{0}/{1} Giochi Caricati",
|
||||
"StatusBarSystemVersion": "Versione di sistema: {0}",
|
||||
"StatusBarXCIFileTrimming": "Trimmando i file XCI '{0}'",
|
||||
"LinuxVmMaxMapCountDialogTitle": "Rilevato limite basso per le mappature di memoria",
|
||||
"LinuxVmMaxMapCountDialogTextPrimary": "Vuoi aumentare il valore di vm.max_map_count a {0}?",
|
||||
"LinuxVmMaxMapCountDialogTextSecondary": "Alcuni giochi potrebbero provare a creare più mappature di memoria di quanto sia attualmente consentito. Ryujinx si bloccherà non appena questo limite viene superato.",
|
||||
@ -99,8 +103,8 @@
|
||||
"SettingsTabGeneralEnableDiscordRichPresence": "Attiva Discord Rich Presence",
|
||||
"SettingsTabGeneralCheckUpdatesOnLaunch": "Controlla aggiornamenti all'avvio",
|
||||
"SettingsTabGeneralShowConfirmExitDialog": "Mostra dialogo \"Conferma Uscita\"",
|
||||
"SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
|
||||
"SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)",
|
||||
"SettingsTabGeneralRememberWindowState": "Ricorda Dimensione/Posizione Finestra",
|
||||
"SettingsTabGeneralShowTitleBar": "Mostra barra del titolo (Richiede il riavvio)",
|
||||
"SettingsTabGeneralHideCursor": "Nascondi il cursore:",
|
||||
"SettingsTabGeneralHideCursorNever": "Mai",
|
||||
"SettingsTabGeneralHideCursorOnIdle": "Quando è inattivo",
|
||||
@ -400,6 +404,8 @@
|
||||
"InputDialogTitle": "Finestra di input",
|
||||
"InputDialogOk": "OK",
|
||||
"InputDialogCancel": "Annulla",
|
||||
"InputDialogCancelling": "Cancellando",
|
||||
"InputDialogClose": "Chiudi",
|
||||
"InputDialogAddNewProfileTitle": "Scegli il nome del profilo",
|
||||
"InputDialogAddNewProfileHeader": "Digita un nome profilo",
|
||||
"InputDialogAddNewProfileSubtext": "(Lunghezza massima: {0})",
|
||||
@ -407,7 +413,7 @@
|
||||
"AvatarSetBackgroundColor": "Imposta colore di sfondo",
|
||||
"AvatarClose": "Chiudi",
|
||||
"ControllerSettingsLoadProfileToolTip": "Carica profilo",
|
||||
"ControllerSettingsViewProfileToolTip": "View Profile",
|
||||
"ControllerSettingsViewProfileToolTip": "Visualizza profilo",
|
||||
"ControllerSettingsAddProfileToolTip": "Aggiungi profilo",
|
||||
"ControllerSettingsRemoveProfileToolTip": "Rimuovi profilo",
|
||||
"ControllerSettingsSaveProfileToolTip": "Salva profilo",
|
||||
@ -417,7 +423,7 @@
|
||||
"GameListContextMenuToggleFavorite": "Preferito",
|
||||
"GameListContextMenuToggleFavoriteToolTip": "Segna il gioco come preferito",
|
||||
"SettingsTabGeneralTheme": "Tema:",
|
||||
"SettingsTabGeneralThemeAuto": "Auto",
|
||||
"SettingsTabGeneralThemeAuto": "Automatico",
|
||||
"SettingsTabGeneralThemeDark": "Scuro",
|
||||
"SettingsTabGeneralThemeLight": "Chiaro",
|
||||
"ControllerSettingsConfigureGeneral": "Configura",
|
||||
@ -456,7 +462,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "Non sei connesso ad Internet!",
|
||||
"DialogUpdaterNoInternetSubMessage": "Verifica di avere una connessione ad Internet funzionante!",
|
||||
"DialogUpdaterDirtyBuildMessage": "Non puoi aggiornare una Dirty build di Ryujinx!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Scarica Ryujinx da https://https://github.com/GreemDev/Ryujinx/releases/ se stai cercando una versione supportata.",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Scarica Ryujinx da https://ryujinx.app/download se stai cercando una versione supportata.",
|
||||
"DialogRestartRequiredMessage": "Riavvio richiesto",
|
||||
"DialogThemeRestartMessage": "Il tema è stato salvato. È richiesto un riavvio per applicare il tema.",
|
||||
"DialogThemeRestartSubMessage": "Vuoi riavviare?",
|
||||
@ -469,6 +475,7 @@
|
||||
"DialogUninstallFileTypesSuccessMessage": "Tipi di file disinstallati con successo!",
|
||||
"DialogUninstallFileTypesErrorMessage": "Disinstallazione dei tipi di file non riuscita.",
|
||||
"DialogOpenSettingsWindowLabel": "Apri finestra delle impostazioni",
|
||||
"DialogOpenXCITrimmerWindowLabel": "Finestra XCI Trimmer",
|
||||
"DialogControllerAppletTitle": "Applet del controller",
|
||||
"DialogMessageDialogErrorExceptionMessage": "Errore nella visualizzazione del Message Dialog: {0}",
|
||||
"DialogSoftwareKeyboardErrorExceptionMessage": "Errore nella visualizzazione della tastiera software: {0}",
|
||||
@ -522,7 +529,7 @@
|
||||
"DialogModManagerDeletionAllWarningMessage": "Stai per eliminare tutte le mod per questo titolo.\n\nVuoi davvero procedere?",
|
||||
"SettingsTabGraphicsFeaturesOptions": "Funzionalità",
|
||||
"SettingsTabGraphicsBackendMultithreading": "Multithreading del backend grafico:",
|
||||
"CommonAuto": "Auto",
|
||||
"CommonAuto": "Automatico",
|
||||
"CommonOff": "Disattivato",
|
||||
"CommonOn": "Attivo",
|
||||
"InputDialogYes": "Sì",
|
||||
@ -669,9 +676,15 @@
|
||||
"OpenSetupGuideMessage": "Apri la guida all'installazione",
|
||||
"NoUpdate": "Nessun aggiornamento",
|
||||
"TitleUpdateVersionLabel": "Versione {0}",
|
||||
"TitleBundledUpdateVersionLabel": "Incluso: Version {0}",
|
||||
"TitleBundledDlcLabel": "Incluso:",
|
||||
"RyujinxInfo": "Ryujinx - Info",
|
||||
"TitleBundledUpdateVersionLabel": "In bundle: Versione {0}",
|
||||
"TitleBundledDlcLabel": "In bundle:",
|
||||
"TitleXCIStatusPartialLabel": "Parziale",
|
||||
"TitleXCIStatusTrimmableLabel": "Non Trimmato",
|
||||
"TitleXCIStatusUntrimmableLabel": "Trimmato",
|
||||
"TitleXCIStatusFailedLabel": "(Fallito)",
|
||||
"TitleXCICanSaveLabel": "Salva {0:n0} Mb",
|
||||
"TitleXCISavingLabel": "Salva {0:n0} Mb",
|
||||
"RyujinxInfo": "Ryujinx - Informazioni",
|
||||
"RyujinxConfirm": "Ryujinx - Conferma",
|
||||
"FileDialogAllTypes": "Tutti i tipi",
|
||||
"Never": "Mai",
|
||||
@ -723,27 +736,56 @@
|
||||
"SelectDlcDialogTitle": "Seleziona file dei DLC",
|
||||
"SelectUpdateDialogTitle": "Seleziona file di aggiornamento",
|
||||
"SelectModDialogTitle": "Seleziona cartella delle mod",
|
||||
"TrimXCIFileDialogTitle": "Controlla e Trimma i file XCI ",
|
||||
"TrimXCIFileDialogPrimaryText": "Questa funzionalita controllerà prima lo spazio libero e poi trimmerà il file XCI per liberare dello spazio.",
|
||||
"TrimXCIFileDialogSecondaryText": "Dimensioni Attuali File: {0:n} MB\nDimensioni Dati Gioco: {1:n} MB\nRisparimio Spazio Disco: {2:n} MB",
|
||||
"TrimXCIFileNoTrimNecessary": "Il file XCI non deve essere trimmato. Controlla i log per ulteriori dettagli",
|
||||
"TrimXCIFileNoUntrimPossible": "Il file XCI non può essere untrimmato. Controlla i log per ulteriori dettagli",
|
||||
"TrimXCIFileReadOnlyFileCannotFix": "Il file XCI è in sola lettura e non può essere reso Scrivibile. Controlla i log per ulteriori dettagli",
|
||||
"TrimXCIFileFileSizeChanged": "Il file XCI ha cambiato dimensioni da quando è stato scansionato. Controlla che il file non stia venendo scritto da qualche altro programma e poi riprova.",
|
||||
"TrimXCIFileFreeSpaceCheckFailed": "Il file XCI ha dati nello spazio libero, non è sicuro effettuare il trimming",
|
||||
"TrimXCIFileInvalidXCIFile": "Il file XCI contiene dati invlidi. Controlla i log per ulteriori dettagli",
|
||||
"TrimXCIFileFileIOWriteError": "Il file XCI non può essere aperto per essere scritto. Controlla i log per ulteriori dettagli",
|
||||
"TrimXCIFileFailedPrimaryText": "Trimming del file XCI fallito",
|
||||
"TrimXCIFileCancelled": "Operazione Cancellata",
|
||||
"TrimXCIFileFileUndertermined": "Nessuna operazione è stata effettuata",
|
||||
"UserProfileWindowTitle": "Gestione profili utente",
|
||||
"CheatWindowTitle": "Gestione trucchi",
|
||||
"DlcWindowTitle": "Gestisci DLC per {0} ({1})",
|
||||
"ModWindowTitle": "Gestisci mod per {0} ({1})",
|
||||
"UpdateWindowTitle": "Gestione aggiornamenti",
|
||||
"XCITrimmerWindowTitle": "XCI File Trimmer",
|
||||
"XCITrimmerTitleStatusCount": "{0} di {1} Titolo(i) Selezionati",
|
||||
"XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Titolo(i) Selezionati ({2} visualizzato)",
|
||||
"XCITrimmerTitleStatusTrimming": "Trimming {0} Titolo(i)...",
|
||||
"XCITrimmerTitleStatusUntrimming": "Untrimming {0} Titolo(i)...",
|
||||
"XCITrimmerTitleStatusFailed": "Fallito",
|
||||
"XCITrimmerPotentialSavings": "Potenziali Salvataggi",
|
||||
"XCITrimmerActualSavings": "Effettivi Salvataggi",
|
||||
"XCITrimmerSavingsMb": "{0:n0} Mb",
|
||||
"XCITrimmerSelectDisplayed": "Seleziona Visualizzati",
|
||||
"XCITrimmerDeselectDisplayed": "Deselziona Visualizzati",
|
||||
"XCITrimmerSortName": "Titolo",
|
||||
"XCITrimmerSortSaved": "Salvataggio Spazio",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} aggiornamento/i aggiunto/i",
|
||||
"UpdateWindowBundledContentNotice": "Gli aggiornamenti inclusi non possono essere eliminati, ma solo disattivati",
|
||||
"CheatWindowHeading": "Trucchi disponibili per {0} [{1}]",
|
||||
"BuildId": "ID Build",
|
||||
"DlcWindowBundledContentNotice": "i DLC \"impacchettati\" non possono essere rimossi, ma solo disabilitati.",
|
||||
"DlcWindowHeading": "DLC disponibili per {0} [{1}]",
|
||||
"ModWindowHeading": "{0} mod",
|
||||
"UserProfilesEditProfile": "Modifica selezionati",
|
||||
"Cancel": "Annulla",
|
||||
"Save": "Salva",
|
||||
"Discard": "Scarta",
|
||||
"UpdateWindowBundledContentNotice": "Gli aggiornamenti inclusi non possono essere eliminati, ma solo disattivati",
|
||||
"DlcWindowDlcAddedMessage": "{0} nuovo/i contenuto/i scaricabile/i aggiunto/i",
|
||||
"AutoloadDlcAddedMessage": "{0} contenuto/i scaricabile/i aggiunto/i",
|
||||
"AutoloadDlcRemovedMessage": "{0} contenuto/i scaricabile/i mancante/i rimosso/i",
|
||||
"AutoloadUpdateAddedMessage": "{0} aggiornamento/i aggiunto/i",
|
||||
"AutoloadUpdateRemovedMessage": "{0} aggiornamento/i mancante/i rimosso/i",
|
||||
"DlcWindowBundledContentNotice": "i DLC \"impacchettati\" non possono essere rimossi, ma solo disabilitati.",
|
||||
"DlcWindowDlcAddedMessage": "{0} nuovo/i contenuto/i scaricabile/i aggiunto/i",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} aggiornamento/i aggiunto/i",
|
||||
"ModWindowHeading": "{0} mod",
|
||||
"UserProfilesEditProfile": "Modifica selezionati",
|
||||
"Continue": "Continua",
|
||||
"Cancel": "Annulla",
|
||||
"Save": "Salva",
|
||||
"Discard": "Scarta",
|
||||
"Paused": "In pausa",
|
||||
"UserProfilesSetProfileImage": "Imposta immagine profilo",
|
||||
"UserProfileEmptyNameError": "Il nome è obbligatorio",
|
||||
|
@ -456,7 +456,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "インターネットに接続されていません!",
|
||||
"DialogUpdaterNoInternetSubMessage": "インターネット接続が正常動作しているか確認してください!",
|
||||
"DialogUpdaterDirtyBuildMessage": "Dirty ビルドの Ryujinx はアップデートできません!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "サポートされているバージョンをお探しなら, https://https://github.com/GreemDev/Ryujinx/releases/ で Ryujinx をダウンロードしてください.",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "サポートされているバージョンをお探しなら, https://ryujinx.app/download で Ryujinx をダウンロードしてください.",
|
||||
"DialogRestartRequiredMessage": "再起動が必要",
|
||||
"DialogThemeRestartMessage": "テーマがセーブされました. テーマを適用するには再起動が必要です.",
|
||||
"DialogThemeRestartSubMessage": "再起動しますか",
|
||||
@ -728,6 +728,8 @@
|
||||
"DlcWindowTitle": "DLC 管理",
|
||||
"ModWindowTitle": "Manage Mods for {0} ({1})",
|
||||
"UpdateWindowTitle": "アップデート管理",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||
"CheatWindowHeading": "利用可能なチート {0} [{1}]",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -456,7 +456,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "Nie masz połączenia z Internetem!",
|
||||
"DialogUpdaterNoInternetSubMessage": "Sprawdź, czy masz działające połączenie internetowe!",
|
||||
"DialogUpdaterDirtyBuildMessage": "Nie możesz zaktualizować Dirty wersji Ryujinx!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Pobierz Ryujinx ze strony https://https://github.com/GreemDev/Ryujinx/releases/, jeśli szukasz obsługiwanej wersji.",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Pobierz Ryujinx ze strony https://ryujinx.app/download, jeśli szukasz obsługiwanej wersji.",
|
||||
"DialogRestartRequiredMessage": "Wymagane Ponowne Uruchomienie",
|
||||
"DialogThemeRestartMessage": "Motyw został zapisany. Aby zastosować motyw, konieczne jest ponowne uruchomienie.",
|
||||
"DialogThemeRestartSubMessage": "Czy chcesz uruchomić ponownie?",
|
||||
@ -728,6 +728,8 @@
|
||||
"DlcWindowTitle": "Menedżer Zawartości do Pobrania",
|
||||
"ModWindowTitle": "Zarządzaj modami dla {0} ({1})",
|
||||
"UpdateWindowTitle": "Menedżer Aktualizacji Tytułu",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
|
||||
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
|
||||
"CheatWindowHeading": "Kody Dostępne dla {0} [{1}]",
|
||||
|
@ -456,7 +456,7 @@
|
||||
"DialogUpdaterNoInternetMessage": "Você não está conectado à Internet!",
|
||||
"DialogUpdaterNoInternetSubMessage": "Por favor, certifique-se de que você tem uma conexão funcional à Internet!",
|
||||
"DialogUpdaterDirtyBuildMessage": "Você não pode atualizar uma compilação Dirty do Ryujinx!",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Por favor, baixe o Ryujinx em https://https://github.com/GreemDev/Ryujinx/releases/ se está procurando por uma versão suportada.",
|
||||
"DialogUpdaterDirtyBuildSubMessage": "Por favor, baixe o Ryujinx em https://ryujinx.app/download se está procurando por uma versão suportada.",
|
||||
"DialogRestartRequiredMessage": "Reinicialização necessária",
|
||||
"DialogThemeRestartMessage": "O tema foi salvo. Uma reinicialização é necessária para aplicar o tema.",
|
||||
"DialogThemeRestartSubMessage": "Deseja reiniciar?",
|
||||
@ -728,6 +728,8 @@
|
||||
"DlcWindowTitle": "Gerenciador de DLC",
|
||||
"ModWindowTitle": "Gerenciar Mods para {0} ({1})",
|
||||
"UpdateWindowTitle": "Gerenciador de atualizações",
|
||||
"XCITrimmerTrim": "Trim",
|
||||
"XCITrimmerUntrim": "Untrim",
|
||||
"UpdateWindowUpdateAddedMessage": "{0} nova(s) atualização(ões) adicionada(s)",
|
||||
"UpdateWindowBundledContentNotice": "Atualizações incorporadas não podem ser removidas, apenas desativadas.",
|
||||
"CheatWindowHeading": "Cheats disponíveis para {0} [{1}]",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user