diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml
index b64fc1d92..d102beaa3 100644
--- a/.github/workflows/canary.yml
+++ b/.github/workflows/canary.yml
@@ -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
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7a78718be..183d4c618 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
@@ -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
diff --git a/Directory.Packages.props b/Directory.Packages.props
index fff045062..c0ace079d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -33,6 +33,7 @@
+
diff --git a/src/ARMeilleure/Translation/Dominance.cs b/src/ARMeilleure/Translation/Dominance.cs
index e2185bd85..b62714fdf 100644
--- a/src/ARMeilleure/Translation/Dominance.cs
+++ b/src/ARMeilleure/Translation/Dominance.cs
@@ -77,7 +77,7 @@ namespace ARMeilleure.Translation
{
continue;
}
-
+
for (int pBlkIndex = 0; pBlkIndex < block.Predecessors.Count; pBlkIndex++)
{
BasicBlock current = block.Predecessors[pBlkIndex];
diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs
index 6dd38ed89..8236150fe 100644
--- a/src/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/src/ARMeilleure/Translation/PTC/Ptc.cs
@@ -13,6 +13,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
+using System.Linq;
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -848,18 +849,15 @@ namespace ARMeilleure.Translation.PTC
}
}
- List threads = new();
- for (int i = 0; i < degreeOfParallelism; i++)
- {
- Thread thread = new(TranslateFuncs)
- {
- IsBackground = true,
- Name = "Ptc.TranslateThread." + i
- };
-
- threads.Add(thread);
- }
+ List threads = Enumerable.Range(0, degreeOfParallelism)
+ .Select(idx =>
+ new Thread(TranslateFuncs)
+ {
+ IsBackground = true,
+ Name = "Ptc.TranslateThread." + idx
+ }
+ ).ToList();
Stopwatch sw = Stopwatch.StartNew();
diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
index 69f7d876d..be0e1518c 100644
--- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
+++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
@@ -3,6 +3,7 @@ namespace Ryujinx.Common.Configuration.Multiplayer
public enum MultiplayerMode
{
Disabled,
+ LdnRyu,
LdnMitm,
}
}
diff --git a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
index 02c6dc97b..a9dbe646a 100644
--- a/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
+++ b/src/Ryujinx.Common/Logging/Targets/AsyncLogTargetWrapper.cs
@@ -30,10 +30,10 @@ namespace Ryujinx.Common.Logging.Targets
string ILogTarget.Name { get => _target.Name; }
public AsyncLogTargetWrapper(ILogTarget target)
- : this(target, -1, AsyncLogTargetOverflowAction.Block)
+ : this(target, -1)
{ }
- public AsyncLogTargetWrapper(ILogTarget target, int queueLimit, AsyncLogTargetOverflowAction overflowAction)
+ public AsyncLogTargetWrapper(ILogTarget target, int queueLimit = -1, AsyncLogTargetOverflowAction overflowAction = AsyncLogTargetOverflowAction.Block)
{
_target = target;
_messageQueue = new BlockingCollection(queueLimit);
diff --git a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
index 8d4ede96c..631df3056 100644
--- a/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
+++ b/src/Ryujinx.Common/Logging/Targets/FileLogTarget.cs
@@ -47,7 +47,7 @@ namespace Ryujinx.Common.Logging.Targets
}
// Clean up old logs, should only keep 3
- FileInfo[] files = logDir.GetFiles("*.log").OrderBy((info => info.CreationTime)).ToArray();
+ FileInfo[] files = logDir.GetFiles("*.log").OrderBy(info => info.CreationTime).ToArray();
for (int i = 0; i < files.Length - 2; i++)
{
try
diff --git a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
index 762c73889..fcb2229a7 100644
--- a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
+++ b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
@@ -803,18 +803,6 @@ namespace Ryujinx.Common.Memory
public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
- public struct Array256 : IArray where T : unmanaged
- {
- T _e0;
- Array128 _other;
- Array127 _other2;
- public readonly int Length => 256;
- public ref T this[int index] => ref AsSpan()[index];
-
- [Pure]
- public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
- }
-
public struct Array140 : IArray where T : unmanaged
{
T _e0;
@@ -828,6 +816,18 @@ namespace Ryujinx.Common.Memory
public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
}
+ public struct Array256 : IArray where T : unmanaged
+ {
+ T _e0;
+ Array128 _other;
+ Array127 _other2;
+ public readonly int Length => 256;
+ public ref T this[int index] => ref AsSpan()[index];
+
+ [Pure]
+ public Span AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
+ }
+
public struct Array384 : IArray where T : unmanaged
{
T _e0;
diff --git a/src/Ryujinx.Common/ReactiveObject.cs b/src/Ryujinx.Common/ReactiveObject.cs
index 4f27af546..8df1e20fe 100644
--- a/src/Ryujinx.Common/ReactiveObject.cs
+++ b/src/Ryujinx.Common/ReactiveObject.cs
@@ -1,11 +1,13 @@
+using Ryujinx.Common.Logging;
using System;
+using System.Globalization;
using System.Threading;
namespace Ryujinx.Common
{
public class ReactiveObject
{
- private readonly ReaderWriterLockSlim _readerWriterLock = new();
+ private readonly ReaderWriterLockSlim _rwLock = new();
private bool _isInitialized;
private T _value;
@@ -15,15 +17,15 @@ namespace Ryujinx.Common
{
get
{
- _readerWriterLock.EnterReadLock();
+ _rwLock.EnterReadLock();
T value = _value;
- _readerWriterLock.ExitReadLock();
+ _rwLock.ExitReadLock();
return value;
}
set
{
- _readerWriterLock.EnterWriteLock();
+ _rwLock.EnterWriteLock();
T oldValue = _value;
@@ -32,7 +34,7 @@ namespace Ryujinx.Common
_isInitialized = true;
_value = value;
- _readerWriterLock.ExitWriteLock();
+ _rwLock.ExitWriteLock();
if (!oldIsInitialized || oldValue == null || !oldValue.Equals(_value))
{
@@ -40,12 +42,22 @@ namespace Ryujinx.Common
}
}
}
+
+ public void LogChangesToValue(string valueName, LogClass logClass = LogClass.Configuration)
+ => Event += (_, e) => ReactiveObjectHelper.LogValueChange(logClass, e, valueName);
public static implicit operator T(ReactiveObject obj) => obj.Value;
}
public static class ReactiveObjectHelper
{
+ public static void LogValueChange(LogClass logClass, ReactiveEventArgs eventArgs, string valueName)
+ {
+ string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}");
+
+ Logger.Info?.Print(logClass, message);
+ }
+
public static void Toggle(this ReactiveObject rBoolean) => rBoolean.Value = !rBoolean.Value;
}
diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs
index 523479d82..f4c62155a 100644
--- a/src/Ryujinx.Common/ReleaseInformation.cs
+++ b/src/Ryujinx.Common/ReleaseInformation.cs
@@ -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("%%");
diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
index 71e02184e..53d1e4f33 100644
--- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
+++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
@@ -1,6 +1,7 @@
using System.Buffers.Binary;
using System.Net;
using System.Net.NetworkInformation;
+using System.Runtime.InteropServices;
namespace Ryujinx.Common.Utilities
{
@@ -65,6 +66,11 @@ namespace Ryujinx.Common.Utilities
return (targetProperties, targetAddressInfo);
}
+ public static bool SupportsDynamicDns()
+ {
+ return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ }
+
public static uint ConvertIpv4Address(IPAddress ipAddress)
{
return BinaryPrimitives.ReadUInt32BigEndian(ipAddress.GetAddressBytes());
diff --git a/src/Ryujinx.Graphics.GAL/IRenderer.cs b/src/Ryujinx.Graphics.GAL/IRenderer.cs
index 9b5e2cc42..c2fdcbe4b 100644
--- a/src/Ryujinx.Graphics.GAL/IRenderer.cs
+++ b/src/Ryujinx.Graphics.GAL/IRenderer.cs
@@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.GAL
IPipeline Pipeline { get; }
IWindow Window { get; }
-
+
uint ProgramCount { get; }
void BackgroundContextAction(Action action, bool alwaysBackground = false);
diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
index 2deee045c..6ead314fd 100644
--- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
+++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
@@ -97,7 +97,7 @@ namespace Ryujinx.Graphics.OpenGL
public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info)
{
ProgramCount++;
-
+
return new Program(shaders, info.FragmentOutputMap);
}
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index 8d324957a..cc2bc36c2 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -549,7 +549,7 @@ namespace Ryujinx.Graphics.Vulkan
public IProgram CreateProgram(ShaderSource[] sources, ShaderInfo info)
{
ProgramCount++;
-
+
bool isCompute = sources.Length == 1 && sources[0].Stage == ShaderStage.Compute;
if (info.State.HasValue || isCompute)
diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs
index 955fee4b5..70fcf278d 100644
--- a/src/Ryujinx.HLE/HLEConfiguration.cs
+++ b/src/Ryujinx.HLE/HLEConfiguration.cs
@@ -164,6 +164,21 @@ namespace Ryujinx.HLE
///
public MultiplayerMode MultiplayerMode { internal get; set; }
+ ///
+ /// Disable P2P mode
+ ///
+ public bool MultiplayerDisableP2p { internal get; set; }
+
+ ///
+ /// Multiplayer Passphrase
+ ///
+ public string MultiplayerLdnPassphrase { internal get; set; }
+
+ ///
+ /// LDN Server
+ ///
+ public string MultiplayerLdnServer { internal get; set; }
+
///
/// An action called when HLE force a refresh of output after docked mode changed.
///
@@ -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;
}
}
}
diff --git a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs
index 171a083f3..2e7b8ee76 100644
--- a/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs
+++ b/src/Ryujinx.HLE/HOS/Diagnostics/Demangler/Demangler.cs
@@ -2463,7 +2463,7 @@ namespace Ryujinx.HLE.HOS.Diagnostics.Demangler
return ParseIntegerLiteral("unsigned short");
case 'i':
_position++;
- return ParseIntegerLiteral("");
+ return ParseIntegerLiteral(string.Empty);
case 'j':
_position++;
return ParseIntegerLiteral("u");
diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs
index ee179c929..7cbe1afca 100644
--- a/src/Ryujinx.HLE/HOS/ModLoader.cs
+++ b/src/Ryujinx.HLE/HOS/ModLoader.cs
@@ -116,18 +116,13 @@ namespace Ryujinx.HLE.HOS
private readonly Dictionary _appMods; // key is ApplicationId
private PatchCache _patches;
- private static readonly EnumerationOptions _dirEnumOptions;
-
- static ModLoader()
+ private static readonly EnumerationOptions _dirEnumOptions = new()
{
- _dirEnumOptions = new EnumerationOptions
- {
- MatchCasing = MatchCasing.CaseInsensitive,
- MatchType = MatchType.Simple,
- RecurseSubdirectories = false,
- ReturnSpecialDirectories = false,
- };
- }
+ MatchCasing = MatchCasing.CaseInsensitive,
+ MatchType = MatchType.Simple,
+ RecurseSubdirectories = false,
+ ReturnSpecialDirectories = false,
+ };
public ModLoader()
{
@@ -169,7 +164,7 @@ namespace Ryujinx.HLE.HOS
foreach (var modDir in dir.EnumerateDirectories())
{
types.Clear();
- Mod mod = new("", null, true);
+ Mod mod = new(string.Empty, null, true);
if (StrEquals(RomfsDir, modDir.Name))
{
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs
index 4da5fe42b..c6d6ac944 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs
@@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
- [StructLayout(LayoutKind.Sequential, Size = 0x20)]
+ [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 8)]
struct NetworkConfig
{
public IntentId IntentId;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs
index 449c923cc..f3ab1edd5 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs
@@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
- [StructLayout(LayoutKind.Sequential, Size = 0x60)]
+ [StructLayout(LayoutKind.Sequential, Size = 0x60, Pack = 8)]
struct ScanFilter
{
public NetworkId NetworkId;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
index 5939a1394..f3968aab4 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
@@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
- [StructLayout(LayoutKind.Sequential, Size = 0x44)]
+ [StructLayout(LayoutKind.Sequential, Size = 0x44, Pack = 2)]
struct SecurityConfig
{
public SecurityMode SecurityMode;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs
index dbcaa9eeb..e564a2ec9 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs
@@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
- [StructLayout(LayoutKind.Sequential, Size = 0x20)]
+ [StructLayout(LayoutKind.Sequential, Size = 0x20, Pack = 1)]
struct SecurityParameter
{
public Array16 Data;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
index 3820f936e..7246f6f80 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
@@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
- [StructLayout(LayoutKind.Sequential, Size = 0x30)]
+ [StructLayout(LayoutKind.Sequential, Size = 0x30, Pack = 1)]
struct UserConfig
{
public Array33 UserName;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
index 78ebcac82..bd00a3139 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
@@ -15,6 +15,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public Array8 LatestUpdates = new();
public bool Connected { get; private set; }
+ public ProxyConfig Config => _parent.NetworkClient.Config;
+
public AccessPoint(IUserLocalCommunicationService parent)
{
_parent = parent;
@@ -24,9 +26,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public void Dispose()
{
- _parent.NetworkClient.DisconnectNetwork();
+ if (_parent?.NetworkClient != null)
+ {
+ _parent.NetworkClient.DisconnectNetwork();
- _parent.NetworkClient.NetworkChange -= NetworkChanged;
+ _parent.NetworkClient.NetworkChange -= NetworkChanged;
+ }
}
private void NetworkChanged(object sender, NetworkChangeEventArgs e)
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
index 7ad6de51d..028ab6cfc 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
@@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
interface INetworkClient : IDisposable
{
+ ProxyConfig Config { get; }
bool NeedsRealId { get; }
event EventHandler NetworkChange;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
index 1d4b5485e..9f65aed4b 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
@@ -9,6 +9,8 @@ using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using Ryujinx.Horizon.Common;
using Ryujinx.Memory;
using System;
@@ -21,6 +23,9 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
class IUserLocalCommunicationService : IpcService, IDisposable
{
+ public static string DefaultLanPlayHost = "ryuldn.vudjun.com";
+ public static short LanPlayPort = 30456;
+
public INetworkClient NetworkClient { get; private set; }
private const int NifmRequestID = 90;
@@ -175,19 +180,37 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected)
{
- (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
-
- if (unicastAddress == null)
+ ProxyConfig config = _state switch
{
- context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
- context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
+ NetworkState.AccessPointCreated => _accessPoint.Config,
+ NetworkState.StationConnected => _station.Config,
+
+ _ => default
+ };
+
+ if (config.ProxyIp == 0)
+ {
+ (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface(context.Device.Configuration.MultiplayerLanInterfaceId);
+
+ if (unicastAddress == null)
+ {
+ context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultIPAddress));
+ context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(DefaultSubnetMask));
+ }
+ else
+ {
+ Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
+
+ context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
+ context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
+ }
}
else
{
- Logger.Info?.Print(LogClass.ServiceLdn, $"Console's LDN IP is \"{unicastAddress.Address}\".");
+ Logger.Info?.Print(LogClass.ServiceLdn, $"LDN obtained proxy IP.");
- context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.Address));
- context.ResponseData.Write(NetworkHelpers.ConvertIpv4Address(unicastAddress.IPv4Mask));
+ context.ResponseData.Write(config.ProxyIp);
+ context.ResponseData.Write(config.ProxySubnetMask);
}
}
else
@@ -1066,6 +1089,27 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
switch (mode)
{
+ case MultiplayerMode.LdnRyu:
+ try
+ {
+ string ldnServer = context.Device.Configuration.MultiplayerLdnServer;
+ if (string.IsNullOrEmpty(ldnServer))
+ {
+ ldnServer = DefaultLanPlayHost;
+ }
+ if (!IPAddress.TryParse(ldnServer, out IPAddress ipAddress))
+ {
+ ipAddress = Dns.GetHostEntry(ldnServer).AddressList[0];
+ }
+ NetworkClient = new LdnMasterProxyClient(ipAddress.ToString(), LanPlayPort, context.Device.Configuration);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.Print(LogClass.ServiceLdn, "Could not locate LdnRyu server. Defaulting to stubbed wireless.");
+ Logger.Error?.Print(LogClass.ServiceLdn, ex.Message);
+ NetworkClient = new LdnDisabledClient();
+ }
+ break;
case MultiplayerMode.LdnMitm:
NetworkClient = new LdnMitmClient(context.Device.Configuration);
break;
@@ -1103,7 +1147,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_accessPoint?.Dispose();
_accessPoint = null;
- NetworkClient?.Dispose();
+ NetworkClient?.DisconnectAndStop();
NetworkClient = null;
}
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
index e3385a1ed..2e8bb8d83 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
@@ -6,12 +7,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
class LdnDisabledClient : INetworkClient
{
+ public ProxyConfig Config { get; }
public bool NeedsRealId => true;
public event EventHandler NetworkChange;
public NetworkError Connect(ConnectRequest request)
{
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!");
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
return NetworkError.None;
@@ -19,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public NetworkError ConnectPrivate(ConnectPrivateRequest request)
{
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to connect to a network, but Multiplayer is disabled!");
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
return NetworkError.None;
@@ -26,6 +30,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
{
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!");
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
return true;
@@ -33,6 +38,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
{
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to create a network, but Multiplayer is disabled!");
NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
return true;
@@ -49,6 +55,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
{
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "Attempted to scan for networks, but Multiplayer is disabled!");
return Array.Empty();
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs
index 273acdd5e..40697d122 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs
@@ -12,6 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
///
internal class LdnMitmClient : INetworkClient
{
+ public ProxyConfig Config { get; }
public bool NeedsRealId => false;
public event EventHandler NetworkChange;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs
new file mode 100644
index 000000000..a7c435506
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs
@@ -0,0 +1,7 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
+{
+ interface IProxyClient
+ {
+ bool SendAsync(byte[] buffer);
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs
new file mode 100644
index 000000000..4c7814b8e
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs
@@ -0,0 +1,645 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
+using Ryujinx.HLE.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using TcpClient = NetCoreServer.TcpClient;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
+{
+ class LdnMasterProxyClient : TcpClient, INetworkClient, IProxyClient
+ {
+ public bool NeedsRealId => true;
+
+ private static InitializeMessage InitializeMemory = new InitializeMessage();
+
+ private const int InactiveTimeout = 6000;
+ private const int FailureTimeout = 4000;
+ private const int ScanTimeout = 1000;
+
+ private bool _useP2pProxy;
+ private NetworkError _lastError;
+
+ private readonly ManualResetEvent _connected = new ManualResetEvent(false);
+ private readonly ManualResetEvent _error = new ManualResetEvent(false);
+ private readonly ManualResetEvent _scan = new ManualResetEvent(false);
+ private readonly ManualResetEvent _reject = new ManualResetEvent(false);
+ private readonly AutoResetEvent _apConnected = new AutoResetEvent(false);
+
+ private readonly RyuLdnProtocol _protocol;
+ private readonly NetworkTimeout _timeout;
+
+ private readonly List _availableGames = new List();
+ private DisconnectReason _disconnectReason;
+
+ private P2pProxyServer _hostedProxy;
+ private P2pProxyClient _connectedProxy;
+
+ private bool _networkConnected;
+
+ private string _passphrase;
+ private byte[] _gameVersion = new byte[0x10];
+
+ private readonly HLEConfiguration _config;
+
+ public event EventHandler NetworkChange;
+
+ public ProxyConfig Config { get; private set; }
+
+ public LdnMasterProxyClient(string address, int port, HLEConfiguration config) : base(address, port)
+ {
+ if (ProxyHelpers.SupportsNoDelay())
+ {
+ OptionNoDelay = true;
+ }
+
+ _protocol = new RyuLdnProtocol();
+ _timeout = new NetworkTimeout(InactiveTimeout, TimeoutConnection);
+
+ _protocol.Initialize += HandleInitialize;
+ _protocol.Connected += HandleConnected;
+ _protocol.Reject += HandleReject;
+ _protocol.RejectReply += HandleRejectReply;
+ _protocol.SyncNetwork += HandleSyncNetwork;
+ _protocol.ProxyConfig += HandleProxyConfig;
+ _protocol.Disconnected += HandleDisconnected;
+
+ _protocol.ScanReply += HandleScanReply;
+ _protocol.ScanReplyEnd += HandleScanReplyEnd;
+ _protocol.ExternalProxy += HandleExternalProxy;
+
+ _protocol.Ping += HandlePing;
+ _protocol.NetworkError += HandleNetworkError;
+
+ _config = config;
+ _useP2pProxy = !config.MultiplayerDisableP2p;
+ }
+
+ private void TimeoutConnection()
+ {
+ _connected.Reset();
+
+ DisconnectAsync();
+
+ while (IsConnected)
+ {
+ Thread.Yield();
+ }
+ }
+
+ private bool EnsureConnected()
+ {
+ if (IsConnected)
+ {
+ return true;
+ }
+
+ _error.Reset();
+
+ ConnectAsync();
+
+ int index = WaitHandle.WaitAny(new WaitHandle[] { _connected, _error }, FailureTimeout);
+
+ if (IsConnected)
+ {
+ SendAsync(_protocol.Encode(PacketId.Initialize, InitializeMemory));
+ }
+
+ return index == 0 && IsConnected;
+ }
+
+ private void UpdatePassphraseIfNeeded()
+ {
+ string passphrase = _config.MultiplayerLdnPassphrase ?? "";
+ if (passphrase != _passphrase)
+ {
+ _passphrase = passphrase;
+
+ SendAsync(_protocol.Encode(PacketId.Passphrase, StringUtils.GetFixedLengthBytes(passphrase, 0x80, Encoding.UTF8)));
+ }
+ }
+
+ protected override void OnConnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client connected a new session with Id {Id}");
+
+ UpdatePassphraseIfNeeded();
+
+ _connected.Set();
+ }
+
+ protected override void OnDisconnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client disconnected a session with Id {Id}");
+
+ _passphrase = null;
+
+ _connected.Reset();
+
+ if (_networkConnected)
+ {
+ DisconnectInternal();
+ }
+ }
+
+ public void DisconnectAndStop()
+ {
+ _timeout.Dispose();
+
+ DisconnectAsync();
+
+ while (IsConnected)
+ {
+ Thread.Yield();
+ }
+
+ Dispose();
+ }
+
+ protected override void OnReceived(byte[] buffer, long offset, long size)
+ {
+ _protocol.Read(buffer, (int)offset, (int)size);
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LDN TCP client caught an error with code {error}");
+
+ _error.Set();
+ }
+
+
+
+ private void HandleInitialize(LdnHeader header, InitializeMessage initialize)
+ {
+ InitializeMemory = initialize;
+ }
+
+ private void HandleExternalProxy(LdnHeader header, ExternalProxyConfig config)
+ {
+ int length = config.AddressFamily switch
+ {
+ AddressFamily.InterNetwork => 4,
+ AddressFamily.InterNetworkV6 => 16,
+ _ => 0
+ };
+
+ if (length == 0)
+ {
+ return; // Invalid external proxy.
+ }
+
+ IPAddress address = new(config.ProxyIp.AsSpan()[..length].ToArray());
+ P2pProxyClient proxy = new(address.ToString(), config.ProxyPort);
+
+ _connectedProxy = proxy;
+
+ bool success = proxy.PerformAuth(config);
+
+ if (!success)
+ {
+ DisconnectInternal();
+ }
+ }
+
+ private void HandlePing(LdnHeader header, PingMessage ping)
+ {
+ if (ping.Requester == 0) // Server requested.
+ {
+ // Send the ping message back.
+
+ SendAsync(_protocol.Encode(PacketId.Ping, ping));
+ }
+ }
+
+ private void HandleNetworkError(LdnHeader header, NetworkErrorMessage error)
+ {
+ if (error.Error == NetworkError.PortUnreachable)
+ {
+ _useP2pProxy = false;
+ }
+ else
+ {
+ _lastError = error.Error;
+ }
+ }
+
+ private NetworkError ConsumeNetworkError()
+ {
+ NetworkError result = _lastError;
+
+ _lastError = NetworkError.None;
+
+ return result;
+ }
+
+ private void HandleSyncNetwork(LdnHeader header, NetworkInfo info)
+ {
+ NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true));
+ }
+
+ private void HandleConnected(LdnHeader header, NetworkInfo info)
+ {
+ _networkConnected = true;
+ _disconnectReason = DisconnectReason.None;
+
+ _apConnected.Set();
+
+ NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, true));
+ }
+
+ private void HandleDisconnected(LdnHeader header, DisconnectMessage message)
+ {
+ DisconnectInternal();
+ }
+
+ private void HandleReject(LdnHeader header, RejectRequest reject)
+ {
+ // When the client receives a Reject request, we have been rejected and will be disconnected shortly.
+ _disconnectReason = reject.DisconnectReason;
+ }
+
+ private void HandleRejectReply(LdnHeader header)
+ {
+ _reject.Set();
+ }
+
+ private void HandleScanReply(LdnHeader header, NetworkInfo info)
+ {
+ _availableGames.Add(info);
+ }
+
+ private void HandleScanReplyEnd(LdnHeader obj)
+ {
+ _scan.Set();
+ }
+
+ private void DisconnectInternal()
+ {
+ if (_networkConnected)
+ {
+ _networkConnected = false;
+
+ _hostedProxy?.Dispose();
+ _hostedProxy = null;
+
+ _connectedProxy?.Dispose();
+ _connectedProxy = null;
+
+ _apConnected.Reset();
+
+ NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false, _disconnectReason));
+
+ if (IsConnected)
+ {
+ _timeout.RefreshTimeout();
+ }
+ }
+ }
+
+ public void DisconnectNetwork()
+ {
+ if (_networkConnected)
+ {
+ SendAsync(_protocol.Encode(PacketId.Disconnect, new DisconnectMessage()));
+
+ DisconnectInternal();
+ }
+ }
+
+ public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
+ {
+ if (_networkConnected)
+ {
+ _reject.Reset();
+
+ SendAsync(_protocol.Encode(PacketId.Reject, new RejectRequest(disconnectReason, nodeId)));
+
+ int index = WaitHandle.WaitAny(new WaitHandle[] { _reject, _error }, InactiveTimeout);
+
+ if (index == 0)
+ {
+ return (ConsumeNetworkError() != NetworkError.None) ? ResultCode.InvalidState : ResultCode.Success;
+ }
+ }
+
+ return ResultCode.InvalidState;
+ }
+
+ public void SetAdvertiseData(byte[] data)
+ {
+ // TODO: validate we're the owner (the server will do this anyways tho)
+ if (_networkConnected)
+ {
+ SendAsync(_protocol.Encode(PacketId.SetAdvertiseData, data));
+ }
+ }
+
+ public void SetGameVersion(byte[] versionString)
+ {
+ _gameVersion = versionString;
+
+ if (_gameVersion.Length < 0x10)
+ {
+ Array.Resize(ref _gameVersion, 0x10);
+ }
+ }
+
+ public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
+ {
+ // TODO: validate we're the owner (the server will do this anyways tho)
+ if (_networkConnected)
+ {
+ SendAsync(_protocol.Encode(PacketId.SetAcceptPolicy, new SetAcceptPolicyRequest
+ {
+ StationAcceptPolicy = acceptPolicy
+ }));
+ }
+ }
+
+ private void DisposeProxy()
+ {
+ _hostedProxy?.Dispose();
+ _hostedProxy = null;
+ }
+
+ private void ConfigureAccessPoint(ref RyuNetworkConfig request)
+ {
+ _gameVersion.AsSpan().CopyTo(request.GameVersion.AsSpan());
+
+ if (_useP2pProxy)
+ {
+ // Before sending the request, attempt to set up a proxy server.
+ // This can be on a range of private ports, which can be exposed on a range of public
+ // ports via UPnP. If any of this fails, we just fall back to using the master server.
+
+ int i = 0;
+ for (; i < P2pProxyServer.PrivatePortRange; i++)
+ {
+ _hostedProxy = new P2pProxyServer(this, (ushort)(P2pProxyServer.PrivatePortBase + i), _protocol);
+
+ try
+ {
+ _hostedProxy.Start();
+
+ break;
+ }
+ catch (SocketException e)
+ {
+ _hostedProxy.Dispose();
+ _hostedProxy = null;
+
+ if (e.SocketErrorCode != SocketError.AddressAlreadyInUse)
+ {
+ i = P2pProxyServer.PrivatePortRange; // Immediately fail.
+ }
+ }
+ }
+
+ bool openSuccess = i < P2pProxyServer.PrivatePortRange;
+
+ if (openSuccess)
+ {
+ Task natPunchResult = _hostedProxy.NatPunch();
+
+ try
+ {
+ if (natPunchResult.Result != 0)
+ {
+ // Tell the server that we are hosting the proxy.
+ request.ExternalProxyPort = natPunchResult.Result;
+ }
+ }
+ catch (Exception) { }
+
+ if (request.ExternalProxyPort == 0)
+ {
+ Logger.Warning?.Print(LogClass.ServiceLdn, "Failed to open a port with UPnP for P2P connection. Proxying through the master server instead. Expect higher latency.");
+ _hostedProxy.Dispose();
+ }
+ else
+ {
+ Logger.Info?.Print(LogClass.ServiceLdn, $"Created a wireless P2P network on port {request.ExternalProxyPort}.");
+ _hostedProxy.Start();
+
+ (_, UnicastIPAddressInformation unicastAddress) = NetworkHelpers.GetLocalInterface();
+
+ unicastAddress.Address.GetAddressBytes().AsSpan().CopyTo(request.PrivateIp.AsSpan());
+ request.InternalProxyPort = _hostedProxy.PrivatePort;
+ request.AddressFamily = unicastAddress.Address.AddressFamily;
+ }
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.ServiceLdn, "Cannot create a P2P server. Proxying through the master server instead. Expect higher latency.");
+ }
+ }
+ }
+
+ private bool CreateNetworkCommon()
+ {
+ bool signalled = _apConnected.WaitOne(FailureTimeout);
+
+ if (!_useP2pProxy && _hostedProxy != null)
+ {
+ Logger.Warning?.Print(LogClass.ServiceLdn, "Locally hosted proxy server was not externally reachable. Proxying through the master server instead. Expect higher latency.");
+
+ DisposeProxy();
+ }
+
+ if (signalled && _connectedProxy != null)
+ {
+ _connectedProxy.EnsureProxyReady();
+
+ Config = _connectedProxy.ProxyConfig;
+ }
+ else
+ {
+ DisposeProxy();
+ }
+
+ return signalled;
+ }
+
+ public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
+ {
+ _timeout.DisableTimeout();
+
+ ConfigureAccessPoint(ref request.RyuNetworkConfig);
+
+ if (!EnsureConnected())
+ {
+ DisposeProxy();
+
+ return false;
+ }
+
+ UpdatePassphraseIfNeeded();
+
+ SendAsync(_protocol.Encode(PacketId.CreateAccessPoint, request, advertiseData));
+
+ // Send a network change event with dummy data immediately. Necessary to avoid crashes in some games
+ var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo()
+ {
+ Common = new CommonNetworkInfo()
+ {
+ MacAddress = InitializeMemory.MacAddress,
+ Channel = request.NetworkConfig.Channel,
+ LinkLevel = 3,
+ NetworkType = 2,
+ Ssid = new Ssid()
+ {
+ Length = 32
+ }
+ },
+ Ldn = new LdnNetworkInfo()
+ {
+ AdvertiseDataSize = (ushort)advertiseData.Length,
+ AuthenticationId = 0,
+ NodeCount = 1,
+ NodeCountMax = request.NetworkConfig.NodeCountMax,
+ SecurityMode = (ushort)request.SecurityConfig.SecurityMode
+ }
+ }, true);
+ networkChangeEvent.Info.Ldn.Nodes[0] = new NodeInfo()
+ {
+ Ipv4Address = 175243265,
+ IsConnected = 1,
+ LocalCommunicationVersion = request.NetworkConfig.LocalCommunicationVersion,
+ MacAddress = InitializeMemory.MacAddress,
+ NodeId = 0,
+ UserName = request.UserConfig.UserName
+ };
+ "12345678123456781234567812345678"u8.ToArray().CopyTo(networkChangeEvent.Info.Common.Ssid.Name.AsSpan());
+ NetworkChange?.Invoke(this, networkChangeEvent);
+
+ return CreateNetworkCommon();
+ }
+
+ public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
+ {
+ _timeout.DisableTimeout();
+
+ ConfigureAccessPoint(ref request.RyuNetworkConfig);
+
+ if (!EnsureConnected())
+ {
+ DisposeProxy();
+
+ return false;
+ }
+
+ UpdatePassphraseIfNeeded();
+
+ SendAsync(_protocol.Encode(PacketId.CreateAccessPointPrivate, request, advertiseData));
+
+ return CreateNetworkCommon();
+ }
+
+ public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
+ {
+ if (!_networkConnected)
+ {
+ _timeout.RefreshTimeout();
+ }
+
+ _availableGames.Clear();
+
+ int index = -1;
+
+ if (EnsureConnected())
+ {
+ UpdatePassphraseIfNeeded();
+
+ _scan.Reset();
+
+ SendAsync(_protocol.Encode(PacketId.Scan, scanFilter));
+
+ index = WaitHandle.WaitAny(new WaitHandle[] { _scan, _error }, ScanTimeout);
+ }
+
+ if (index != 0)
+ {
+ // An error occurred or timeout. Write 0 games.
+ return Array.Empty();
+ }
+
+ return _availableGames.ToArray();
+ }
+
+ private NetworkError ConnectCommon()
+ {
+ bool signalled = _apConnected.WaitOne(FailureTimeout);
+
+ NetworkError error = ConsumeNetworkError();
+
+ if (error != NetworkError.None)
+ {
+ return error;
+ }
+
+ if (signalled && _connectedProxy != null)
+ {
+ _connectedProxy.EnsureProxyReady();
+
+ Config = _connectedProxy.ProxyConfig;
+ }
+
+ return signalled ? NetworkError.None : NetworkError.ConnectTimeout;
+ }
+
+ public NetworkError Connect(ConnectRequest request)
+ {
+ _timeout.DisableTimeout();
+
+ if (!EnsureConnected())
+ {
+ return NetworkError.Unknown;
+ }
+
+ SendAsync(_protocol.Encode(PacketId.Connect, request));
+
+ var networkChangeEvent = new NetworkChangeEventArgs(new NetworkInfo()
+ {
+ Common = request.NetworkInfo.Common,
+ Ldn = request.NetworkInfo.Ldn
+ }, true);
+
+ NetworkChange?.Invoke(this, networkChangeEvent);
+
+ return ConnectCommon();
+ }
+
+ public NetworkError ConnectPrivate(ConnectPrivateRequest request)
+ {
+ _timeout.DisableTimeout();
+
+ if (!EnsureConnected())
+ {
+ return NetworkError.Unknown;
+ }
+
+ SendAsync(_protocol.Encode(PacketId.ConnectPrivate, request));
+
+ return ConnectCommon();
+ }
+
+ private void HandleProxyConfig(LdnHeader header, ProxyConfig config)
+ {
+ Config = config;
+
+ SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol));
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs
new file mode 100644
index 000000000..5012d5d81
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
+{
+ class NetworkTimeout : IDisposable
+ {
+ private readonly int _idleTimeout;
+ private readonly Action _timeoutCallback;
+ private CancellationTokenSource _cancel;
+
+ private readonly object _lock = new object();
+
+ public NetworkTimeout(int idleTimeout, Action timeoutCallback)
+ {
+ _idleTimeout = idleTimeout;
+ _timeoutCallback = timeoutCallback;
+ }
+
+ private async Task TimeoutTask()
+ {
+ CancellationTokenSource cts;
+
+ lock (_lock)
+ {
+ cts = _cancel;
+ }
+
+ if (cts == null)
+ {
+ return;
+ }
+
+ try
+ {
+ await Task.Delay(_idleTimeout, cts.Token);
+ }
+ catch (TaskCanceledException)
+ {
+ return; // Timeout cancelled.
+ }
+
+ lock (_lock)
+ {
+ // Run the timeout callback. If the cancel token source has been replaced, we have _just_ been cancelled.
+ if (cts == _cancel)
+ {
+ _timeoutCallback();
+ }
+ }
+ }
+
+ public bool RefreshTimeout()
+ {
+ lock (_lock)
+ {
+ _cancel?.Cancel();
+
+ _cancel = new CancellationTokenSource();
+
+ Task.Run(TimeoutTask);
+ }
+
+ return true;
+ }
+
+ public void DisableTimeout()
+ {
+ lock (_lock)
+ {
+ _cancel?.Cancel();
+
+ _cancel = new CancellationTokenSource();
+ }
+ }
+
+ public void Dispose()
+ {
+ DisableTimeout();
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs
new file mode 100644
index 000000000..bc3a5edf2
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
+{
+ public class EphemeralPortPool
+ {
+ private const ushort EphemeralBase = 49152;
+
+ private readonly List _ephemeralPorts = new List();
+
+ private readonly object _lock = new object();
+
+ public ushort Get()
+ {
+ ushort port = EphemeralBase;
+ lock (_lock)
+ {
+ // Starting at the ephemeral port base, return an ephemeral port that is not in use.
+ // Returns 0 if the range is exhausted.
+
+ for (int i = 0; i < _ephemeralPorts.Count; i++)
+ {
+ ushort existingPort = _ephemeralPorts[i];
+
+ if (existingPort > port)
+ {
+ // The port was free - take it.
+ _ephemeralPorts.Insert(i, port);
+
+ return port;
+ }
+
+ port++;
+ }
+
+ if (port != 0)
+ {
+ _ephemeralPorts.Add(port);
+ }
+
+ return port;
+ }
+ }
+
+ public void Return(ushort port)
+ {
+ lock (_lock)
+ {
+ _ephemeralPorts.Remove(port);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs
new file mode 100644
index 000000000..bb390d49a
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs
@@ -0,0 +1,254 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
+{
+ class LdnProxy : IDisposable
+ {
+ public EndPoint LocalEndpoint { get; }
+ public IPAddress LocalAddress { get; }
+
+ private readonly List _sockets = new List();
+ private readonly Dictionary _ephemeralPorts = new Dictionary();
+
+ private readonly IProxyClient _parent;
+ private RyuLdnProtocol _protocol;
+ private readonly uint _subnetMask;
+ private readonly uint _localIp;
+ private readonly uint _broadcast;
+
+ public LdnProxy(ProxyConfig config, IProxyClient client, RyuLdnProtocol protocol)
+ {
+ _parent = client;
+ _protocol = protocol;
+
+ _ephemeralPorts[ProtocolType.Udp] = new EphemeralPortPool();
+ _ephemeralPorts[ProtocolType.Tcp] = new EphemeralPortPool();
+
+ byte[] address = BitConverter.GetBytes(config.ProxyIp);
+ Array.Reverse(address);
+ LocalAddress = new IPAddress(address);
+
+ _subnetMask = config.ProxySubnetMask;
+ _localIp = config.ProxyIp;
+ _broadcast = _localIp | (~_subnetMask);
+
+ RegisterHandlers(protocol);
+ }
+
+ public bool Supported(AddressFamily domain, SocketType type, ProtocolType protocol)
+ {
+ if (protocol == ProtocolType.Tcp)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Tcp proxy networking is untested. Please report this game so that it can be tested.");
+ }
+ return domain == AddressFamily.InterNetwork && (protocol == ProtocolType.Tcp || protocol == ProtocolType.Udp);
+ }
+
+ private void RegisterHandlers(RyuLdnProtocol protocol)
+ {
+ protocol.ProxyConnect += HandleConnectionRequest;
+ protocol.ProxyConnectReply += HandleConnectionResponse;
+ protocol.ProxyData += HandleData;
+ protocol.ProxyDisconnect += HandleDisconnect;
+
+ _protocol = protocol;
+ }
+
+ public void UnregisterHandlers(RyuLdnProtocol protocol)
+ {
+ protocol.ProxyConnect -= HandleConnectionRequest;
+ protocol.ProxyConnectReply -= HandleConnectionResponse;
+ protocol.ProxyData -= HandleData;
+ protocol.ProxyDisconnect -= HandleDisconnect;
+ }
+
+ public ushort GetEphemeralPort(ProtocolType type)
+ {
+ return _ephemeralPorts[type].Get();
+ }
+
+ public void ReturnEphemeralPort(ProtocolType type, ushort port)
+ {
+ _ephemeralPorts[type].Return(port);
+ }
+
+ public void RegisterSocket(LdnProxySocket socket)
+ {
+ lock (_sockets)
+ {
+ _sockets.Add(socket);
+ }
+ }
+
+ public void UnregisterSocket(LdnProxySocket socket)
+ {
+ lock (_sockets)
+ {
+ _sockets.Remove(socket);
+ }
+ }
+
+ private void ForRoutedSockets(ProxyInfo info, Action action)
+ {
+ lock (_sockets)
+ {
+ foreach (LdnProxySocket socket in _sockets)
+ {
+ // Must match protocol and destination port.
+ if (socket.ProtocolType != info.Protocol || socket.LocalEndPoint is not IPEndPoint endpoint || endpoint.Port != info.DestPort)
+ {
+ continue;
+ }
+
+ // We can assume packets routed to us have been sent to our destination.
+ // They will either be sent to us, or broadcast packets.
+
+ action(socket);
+ }
+ }
+ }
+
+ public void HandleConnectionRequest(LdnHeader header, ProxyConnectRequest request)
+ {
+ ForRoutedSockets(request.Info, (socket) =>
+ {
+ socket.HandleConnectRequest(request);
+ });
+ }
+
+ public void HandleConnectionResponse(LdnHeader header, ProxyConnectResponse response)
+ {
+ ForRoutedSockets(response.Info, (socket) =>
+ {
+ socket.HandleConnectResponse(response);
+ });
+ }
+
+ public void HandleData(LdnHeader header, ProxyDataHeader proxyHeader, byte[] data)
+ {
+ ProxyDataPacket packet = new ProxyDataPacket() { Header = proxyHeader, Data = data };
+
+ ForRoutedSockets(proxyHeader.Info, (socket) =>
+ {
+ socket.IncomingData(packet);
+ });
+ }
+
+ public void HandleDisconnect(LdnHeader header, ProxyDisconnectMessage disconnect)
+ {
+ ForRoutedSockets(disconnect.Info, (socket) =>
+ {
+ socket.HandleDisconnect(disconnect);
+ });
+ }
+
+ private uint GetIpV4(IPEndPoint endpoint)
+ {
+ if (endpoint.AddressFamily != AddressFamily.InterNetwork)
+ {
+ throw new NotSupportedException();
+ }
+
+ byte[] address = endpoint.Address.GetAddressBytes();
+ Array.Reverse(address);
+
+ return BitConverter.ToUInt32(address);
+ }
+
+ private ProxyInfo MakeInfo(IPEndPoint localEp, IPEndPoint remoteEP, ProtocolType type)
+ {
+ return new ProxyInfo
+ {
+ SourceIpV4 = GetIpV4(localEp),
+ SourcePort = (ushort)localEp.Port,
+
+ DestIpV4 = GetIpV4(remoteEP),
+ DestPort = (ushort)remoteEP.Port,
+
+ Protocol = type
+ };
+ }
+
+ public void RequestConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
+ {
+ // We must ask the other side to initialize a connection, so they can accept a socket for us.
+
+ ProxyConnectRequest request = new ProxyConnectRequest
+ {
+ Info = MakeInfo(localEp, remoteEp, type)
+ };
+
+ _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnect, request));
+ }
+
+ public void SignalConnected(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
+ {
+ // We must tell the other side that we have accepted their request for connection.
+
+ ProxyConnectResponse request = new ProxyConnectResponse
+ {
+ Info = MakeInfo(localEp, remoteEp, type)
+ };
+
+ _parent.SendAsync(_protocol.Encode(PacketId.ProxyConnectReply, request));
+ }
+
+ public void EndConnection(IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
+ {
+ // We must tell the other side that our connection is dropped.
+
+ ProxyDisconnectMessage request = new ProxyDisconnectMessage
+ {
+ Info = MakeInfo(localEp, remoteEp, type),
+ DisconnectReason = 0 // TODO
+ };
+
+ _parent.SendAsync(_protocol.Encode(PacketId.ProxyDisconnect, request));
+ }
+
+ public int SendTo(ReadOnlySpan buffer, SocketFlags flags, IPEndPoint localEp, IPEndPoint remoteEp, ProtocolType type)
+ {
+ // We send exactly as much as the user wants us to, currently instantly.
+ // TODO: handle over "virtual mtu" (we have a max packet size to worry about anyways). fragment if tcp? throw if udp?
+
+ ProxyDataHeader request = new ProxyDataHeader
+ {
+ Info = MakeInfo(localEp, remoteEp, type),
+ DataLength = (uint)buffer.Length
+ };
+
+ _parent.SendAsync(_protocol.Encode(PacketId.ProxyData, request, buffer.ToArray()));
+
+ return buffer.Length;
+ }
+
+ public bool IsBroadcast(uint ip)
+ {
+ return ip == _broadcast;
+ }
+
+ public bool IsMyself(uint ip)
+ {
+ return ip == _localIp;
+ }
+
+ public void Dispose()
+ {
+ UnregisterHandlers(_protocol);
+
+ lock (_sockets)
+ {
+ foreach (LdnProxySocket socket in _sockets)
+ {
+ socket.ProxyDestroyed();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs
new file mode 100644
index 000000000..ed7a9c751
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs
@@ -0,0 +1,797 @@
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
+using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl;
+using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
+{
+ ///
+ /// This socket is forwarded through a TCP stream that goes through the Ldn server.
+ /// The Ldn server will then route the packets we send (or need to receive) within the virtual adhoc network.
+ ///
+ class LdnProxySocket : ISocketImpl
+ {
+ private readonly LdnProxy _proxy;
+
+ private bool _isListening;
+ private readonly List _listenSockets = new List();
+
+ private readonly Queue _connectRequests = new Queue();
+
+ private readonly AutoResetEvent _acceptEvent = new AutoResetEvent(false);
+ private readonly int _acceptTimeout = -1;
+
+ private readonly Queue _errors = new Queue();
+
+ private readonly AutoResetEvent _connectEvent = new AutoResetEvent(false);
+ private ProxyConnectResponse _connectResponse;
+
+ private int _receiveTimeout = -1;
+ private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false);
+ private readonly Queue _receiveQueue = new Queue();
+
+ // private int _sendTimeout = -1; // Sends are techically instant right now, so not _really_ used.
+
+ private bool _connecting;
+ private bool _broadcast;
+ private bool _readShutdown;
+ // private bool _writeShutdown;
+ private bool _closed;
+
+ private readonly Dictionary _socketOptions = new Dictionary()
+ {
+ { SocketOptionName.Broadcast, 0 }, //TODO: honor this value
+ { SocketOptionName.DontLinger, 0 },
+ { SocketOptionName.Debug, 0 },
+ { SocketOptionName.Error, 0 },
+ { SocketOptionName.KeepAlive, 0 },
+ { SocketOptionName.OutOfBandInline, 0 },
+ { SocketOptionName.ReceiveBuffer, 131072 },
+ { SocketOptionName.ReceiveTimeout, -1 },
+ { SocketOptionName.SendBuffer, 131072 },
+ { SocketOptionName.SendTimeout, -1 },
+ { SocketOptionName.Type, 0 },
+ { SocketOptionName.ReuseAddress, 0 } //TODO: honor this value
+ };
+
+ public EndPoint RemoteEndPoint { get; private set; }
+
+ public EndPoint LocalEndPoint { get; private set; }
+
+ public bool Connected { get; private set; }
+
+ public bool IsBound { get; private set; }
+
+ public AddressFamily AddressFamily { get; }
+
+ public SocketType SocketType { get; }
+
+ public ProtocolType ProtocolType { get; }
+
+ public bool Blocking { get; set; }
+
+ public int Available
+ {
+ get
+ {
+ int result = 0;
+
+ lock (_receiveQueue)
+ {
+ foreach (ProxyDataPacket data in _receiveQueue)
+ {
+ result += data.Data.Length;
+ }
+ }
+
+ return result;
+ }
+ }
+
+ public bool Readable
+ {
+ get
+ {
+ if (_isListening)
+ {
+ lock (_connectRequests)
+ {
+ return _connectRequests.Count > 0;
+ }
+ }
+ else
+ {
+ if (_readShutdown)
+ {
+ return true;
+ }
+
+ lock (_receiveQueue)
+ {
+ return _receiveQueue.Count > 0;
+ }
+ }
+
+ }
+ }
+ public bool Writable => Connected || ProtocolType == ProtocolType.Udp;
+ public bool Error => false;
+
+ public LdnProxySocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, LdnProxy proxy)
+ {
+ AddressFamily = addressFamily;
+ SocketType = socketType;
+ ProtocolType = protocolType;
+
+ _proxy = proxy;
+ _socketOptions[SocketOptionName.Type] = (int)socketType;
+
+ proxy.RegisterSocket(this);
+ }
+
+ private IPEndPoint EnsureLocalEndpoint(bool replace)
+ {
+ if (LocalEndPoint != null)
+ {
+ if (replace)
+ {
+ _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port);
+ }
+ else
+ {
+ return (IPEndPoint)LocalEndPoint;
+ }
+ }
+
+ IPEndPoint localEp = new IPEndPoint(_proxy.LocalAddress, _proxy.GetEphemeralPort(ProtocolType));
+ LocalEndPoint = localEp;
+
+ return localEp;
+ }
+
+ public LdnProxySocket AsAccepted(IPEndPoint remoteEp)
+ {
+ Connected = true;
+ RemoteEndPoint = remoteEp;
+
+ IPEndPoint localEp = EnsureLocalEndpoint(true);
+
+ _proxy.SignalConnected(localEp, remoteEp, ProtocolType);
+
+ return this;
+ }
+
+ private void SignalError(WsaError error)
+ {
+ lock (_errors)
+ {
+ _errors.Enqueue((int)error);
+ }
+ }
+
+ private IPEndPoint GetEndpoint(uint ipv4, ushort port)
+ {
+ byte[] address = BitConverter.GetBytes(ipv4);
+ Array.Reverse(address);
+
+ return new IPEndPoint(new IPAddress(address), port);
+ }
+
+ public void IncomingData(ProxyDataPacket packet)
+ {
+ bool isBroadcast = _proxy.IsBroadcast(packet.Header.Info.DestIpV4);
+
+ if (!_closed && (_broadcast || !isBroadcast))
+ {
+ lock (_receiveQueue)
+ {
+ _receiveQueue.Enqueue(packet);
+ }
+ }
+ }
+
+ public ISocketImpl Accept()
+ {
+ if (!_isListening)
+ {
+ throw new InvalidOperationException();
+ }
+
+ // Accept a pending request to this socket.
+
+ lock (_connectRequests)
+ {
+ if (!Blocking && _connectRequests.Count == 0)
+ {
+ throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
+ }
+ }
+
+ while (true)
+ {
+ _acceptEvent.WaitOne(_acceptTimeout);
+
+ lock (_connectRequests)
+ {
+ while (_connectRequests.Count > 0)
+ {
+ ProxyConnectRequest request = _connectRequests.Dequeue();
+
+ if (_connectRequests.Count > 0)
+ {
+ _acceptEvent.Set(); // Still more accepts to do.
+ }
+
+ // Is this request made for us?
+ IPEndPoint endpoint = GetEndpoint(request.Info.DestIpV4, request.Info.DestPort);
+
+ if (Equals(endpoint, LocalEndPoint))
+ {
+ // Yes - let's accept.
+ IPEndPoint remoteEndpoint = GetEndpoint(request.Info.SourceIpV4, request.Info.SourcePort);
+
+ LdnProxySocket socket = new LdnProxySocket(AddressFamily, SocketType, ProtocolType, _proxy).AsAccepted(remoteEndpoint);
+
+ lock (_listenSockets)
+ {
+ _listenSockets.Add(socket);
+ }
+
+ return socket;
+ }
+ }
+ }
+ }
+ }
+
+ public void Bind(EndPoint localEP)
+ {
+ ArgumentNullException.ThrowIfNull(localEP);
+
+ if (LocalEndPoint != null)
+ {
+ _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port);
+ }
+ var asIPEndpoint = (IPEndPoint)localEP;
+ if (asIPEndpoint.Port == 0)
+ {
+ asIPEndpoint.Port = (ushort)_proxy.GetEphemeralPort(ProtocolType);
+ }
+
+ LocalEndPoint = (IPEndPoint)localEP;
+
+ IsBound = true;
+ }
+
+ public void Close()
+ {
+ _closed = true;
+
+ _proxy.UnregisterSocket(this);
+
+ if (Connected)
+ {
+ Disconnect(false);
+ }
+
+ lock (_listenSockets)
+ {
+ foreach (LdnProxySocket socket in _listenSockets)
+ {
+ socket.Close();
+ }
+ }
+
+ _isListening = false;
+ }
+
+ public void Connect(EndPoint remoteEP)
+ {
+ if (_isListening || !IsBound)
+ {
+ throw new InvalidOperationException();
+ }
+
+ if (remoteEP is not IPEndPoint)
+ {
+ throw new NotSupportedException();
+ }
+
+ IPEndPoint localEp = EnsureLocalEndpoint(true);
+
+ _connecting = true;
+
+ _proxy.RequestConnection(localEp, (IPEndPoint)remoteEP, ProtocolType);
+
+ if (!Blocking && ProtocolType == ProtocolType.Tcp)
+ {
+ throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
+ }
+
+ _connectEvent.WaitOne(); //timeout?
+
+ if (_connectResponse.Info.SourceIpV4 == 0)
+ {
+ throw new SocketException((int)WsaError.WSAECONNREFUSED);
+ }
+
+ _connectResponse = default;
+ }
+
+ public void HandleConnectResponse(ProxyConnectResponse obj)
+ {
+ if (!_connecting)
+ {
+ return;
+ }
+
+ _connecting = false;
+
+ if (_connectResponse.Info.SourceIpV4 != 0)
+ {
+ IPEndPoint remoteEp = GetEndpoint(obj.Info.SourceIpV4, obj.Info.SourcePort);
+ RemoteEndPoint = remoteEp;
+
+ Connected = true;
+ }
+ else
+ {
+ // Connection failed
+
+ SignalError(WsaError.WSAECONNREFUSED);
+ }
+ }
+
+ public void Disconnect(bool reuseSocket)
+ {
+ if (Connected)
+ {
+ ConnectionEnded();
+
+ // The other side needs to be notified that connection ended.
+ _proxy.EndConnection(LocalEndPoint as IPEndPoint, RemoteEndPoint as IPEndPoint, ProtocolType);
+ }
+ }
+
+ private void ConnectionEnded()
+ {
+ if (Connected)
+ {
+ RemoteEndPoint = null;
+ Connected = false;
+ }
+ }
+
+ public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue)
+ {
+ if (optionLevel != SocketOptionLevel.Socket)
+ {
+ throw new NotImplementedException();
+ }
+
+ if (_socketOptions.TryGetValue(optionName, out int result))
+ {
+ byte[] data = BitConverter.GetBytes(result);
+ Array.Copy(data, 0, optionValue, 0, Math.Min(data.Length, optionValue.Length));
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public void Listen(int backlog)
+ {
+ if (!IsBound)
+ {
+ throw new SocketException();
+ }
+
+ _isListening = true;
+ }
+
+ public void HandleConnectRequest(ProxyConnectRequest obj)
+ {
+ lock (_connectRequests)
+ {
+ _connectRequests.Enqueue(obj);
+ }
+
+ _connectEvent.Set();
+ }
+
+ public void HandleDisconnect(ProxyDisconnectMessage message)
+ {
+ Disconnect(false);
+ }
+
+ public int Receive(Span buffer)
+ {
+ EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
+
+ return ReceiveFrom(buffer, SocketFlags.None, ref dummy);
+ }
+
+ public int Receive(Span buffer, SocketFlags flags)
+ {
+ EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
+
+ return ReceiveFrom(buffer, flags, ref dummy);
+ }
+
+ public int Receive(Span buffer, SocketFlags flags, out SocketError socketError)
+ {
+ EndPoint dummy = new IPEndPoint(IPAddress.Any, 0);
+
+ return ReceiveFrom(buffer, flags, out socketError, ref dummy);
+ }
+
+ public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEp)
+ {
+ // We just receive all packets meant for us anyways regardless of EP in the actual implementation.
+ // The point is mostly to return the endpoint that we got the data from.
+
+ if (!Connected && ProtocolType == ProtocolType.Tcp)
+ {
+ throw new SocketException((int)WsaError.WSAECONNRESET);
+ }
+
+ lock (_receiveQueue)
+ {
+ if (_receiveQueue.Count > 0)
+ {
+ return ReceiveFromQueue(buffer, flags, ref remoteEp);
+ }
+ else if (_readShutdown)
+ {
+ return 0;
+ }
+ else if (!Blocking)
+ {
+ throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
+ }
+ }
+
+ int timeout = _receiveTimeout;
+
+ _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout);
+
+ if (!Connected && ProtocolType == ProtocolType.Tcp)
+ {
+ throw new SocketException((int)WsaError.WSAECONNRESET);
+ }
+
+ lock (_receiveQueue)
+ {
+ if (_receiveQueue.Count > 0)
+ {
+ return ReceiveFromQueue(buffer, flags, ref remoteEp);
+ }
+ else if (_readShutdown)
+ {
+ return 0;
+ }
+ else
+ {
+ throw new SocketException((int)WsaError.WSAETIMEDOUT);
+ }
+ }
+ }
+
+ public int ReceiveFrom(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp)
+ {
+ // We just receive all packets meant for us anyways regardless of EP in the actual implementation.
+ // The point is mostly to return the endpoint that we got the data from.
+
+ if (!Connected && ProtocolType == ProtocolType.Tcp)
+ {
+ socketError = SocketError.ConnectionReset;
+ return -1;
+ }
+
+ lock (_receiveQueue)
+ {
+ if (_receiveQueue.Count > 0)
+ {
+ return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp);
+ }
+ else if (_readShutdown)
+ {
+ socketError = SocketError.Success;
+ return 0;
+ }
+ else if (!Blocking)
+ {
+ throw new SocketException((int)WsaError.WSAEWOULDBLOCK);
+ }
+ }
+
+ int timeout = _receiveTimeout;
+
+ _receiveEvent.WaitOne(timeout == 0 ? -1 : timeout);
+
+ if (!Connected && ProtocolType == ProtocolType.Tcp)
+ {
+ throw new SocketException((int)WsaError.WSAECONNRESET);
+ }
+
+ lock (_receiveQueue)
+ {
+ if (_receiveQueue.Count > 0)
+ {
+ return ReceiveFromQueue(buffer, flags, out socketError, ref remoteEp);
+ }
+ else if (_readShutdown)
+ {
+ socketError = SocketError.Success;
+ return 0;
+ }
+ else
+ {
+ socketError = SocketError.TimedOut;
+ return -1;
+ }
+ }
+ }
+
+ private int ReceiveFromQueue(Span buffer, SocketFlags flags, ref EndPoint remoteEp)
+ {
+ int size = buffer.Length;
+
+ // Assumes we have the receive queue lock, and at least one item in the queue.
+ ProxyDataPacket packet = _receiveQueue.Peek();
+
+ remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort);
+
+ bool peek = (flags & SocketFlags.Peek) != 0;
+
+ int read;
+
+ if (packet.Data.Length > size)
+ {
+ read = size;
+
+ // Cannot fit in the output buffer. Copy up to what we've got.
+ packet.Data.AsSpan(0, size).CopyTo(buffer);
+
+ if (ProtocolType == ProtocolType.Udp)
+ {
+ // Udp overflows, loses the data, then throws an exception.
+
+ if (!peek)
+ {
+ _receiveQueue.Dequeue();
+ }
+
+ throw new SocketException((int)WsaError.WSAEMSGSIZE);
+ }
+ else if (ProtocolType == ProtocolType.Tcp)
+ {
+ // Split the data at the buffer boundary. It will stay on the recieve queue.
+
+ byte[] newData = new byte[packet.Data.Length - size];
+ Array.Copy(packet.Data, size, newData, 0, newData.Length);
+
+ packet.Data = newData;
+ }
+ }
+ else
+ {
+ read = packet.Data.Length;
+
+ packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer);
+
+ if (!peek)
+ {
+ _receiveQueue.Dequeue();
+ }
+ }
+
+ return read;
+ }
+
+ private int ReceiveFromQueue(Span buffer, SocketFlags flags, out SocketError socketError, ref EndPoint remoteEp)
+ {
+ int size = buffer.Length;
+
+ // Assumes we have the receive queue lock, and at least one item in the queue.
+ ProxyDataPacket packet = _receiveQueue.Peek();
+
+ remoteEp = GetEndpoint(packet.Header.Info.SourceIpV4, packet.Header.Info.SourcePort);
+
+ bool peek = (flags & SocketFlags.Peek) != 0;
+
+ int read;
+
+ if (packet.Data.Length > size)
+ {
+ read = size;
+
+ // Cannot fit in the output buffer. Copy up to what we've got.
+ packet.Data.AsSpan(0, size).CopyTo(buffer);
+
+ if (ProtocolType == ProtocolType.Udp)
+ {
+ // Udp overflows, loses the data, then throws an exception.
+
+ if (!peek)
+ {
+ _receiveQueue.Dequeue();
+ }
+
+ socketError = SocketError.MessageSize;
+ return -1;
+ }
+ else if (ProtocolType == ProtocolType.Tcp)
+ {
+ // Split the data at the buffer boundary. It will stay on the recieve queue.
+
+ byte[] newData = new byte[packet.Data.Length - size];
+ Array.Copy(packet.Data, size, newData, 0, newData.Length);
+
+ packet.Data = newData;
+ }
+ }
+ else
+ {
+ read = packet.Data.Length;
+
+ packet.Data.AsSpan(0, packet.Data.Length).CopyTo(buffer);
+
+ if (!peek)
+ {
+ _receiveQueue.Dequeue();
+ }
+ }
+
+ socketError = SocketError.Success;
+
+ return read;
+ }
+
+ public int Send(ReadOnlySpan buffer)
+ {
+ // Send to the remote host chosen when we "connect" or "accept".
+ if (!Connected)
+ {
+ throw new SocketException();
+ }
+
+ return SendTo(buffer, SocketFlags.None, RemoteEndPoint);
+ }
+
+ public int Send(ReadOnlySpan buffer, SocketFlags flags)
+ {
+ // Send to the remote host chosen when we "connect" or "accept".
+ if (!Connected)
+ {
+ throw new SocketException();
+ }
+
+ return SendTo(buffer, flags, RemoteEndPoint);
+ }
+
+ public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError)
+ {
+ // Send to the remote host chosen when we "connect" or "accept".
+ if (!Connected)
+ {
+ throw new SocketException();
+ }
+
+ return SendTo(buffer, flags, out socketError, RemoteEndPoint);
+ }
+
+ public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP)
+ {
+ if (!Connected && ProtocolType == ProtocolType.Tcp)
+ {
+ throw new SocketException((int)WsaError.WSAECONNRESET);
+ }
+
+ IPEndPoint localEp = EnsureLocalEndpoint(false);
+
+ if (remoteEP is not IPEndPoint)
+ {
+ throw new NotSupportedException();
+ }
+
+ return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType);
+ }
+
+ public int SendTo(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError, EndPoint remoteEP)
+ {
+ if (!Connected && ProtocolType == ProtocolType.Tcp)
+ {
+ socketError = SocketError.ConnectionReset;
+ return -1;
+ }
+
+ IPEndPoint localEp = EnsureLocalEndpoint(false);
+
+ if (remoteEP is not IPEndPoint)
+ {
+ // throw new NotSupportedException();
+ socketError = SocketError.OperationNotSupported;
+ return -1;
+ }
+
+ socketError = SocketError.Success;
+
+ return _proxy.SendTo(buffer, flags, localEp, (IPEndPoint)remoteEP, ProtocolType);
+ }
+
+ public bool Poll(int microSeconds, SelectMode mode)
+ {
+ return mode switch
+ {
+ SelectMode.SelectRead => Readable,
+ SelectMode.SelectWrite => Writable,
+ SelectMode.SelectError => Error,
+ _ => false
+ };
+ }
+
+ public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue)
+ {
+ if (optionLevel != SocketOptionLevel.Socket)
+ {
+ throw new NotImplementedException();
+ }
+
+ switch (optionName)
+ {
+ case SocketOptionName.SendTimeout:
+ //_sendTimeout = optionValue;
+ break;
+ case SocketOptionName.ReceiveTimeout:
+ _receiveTimeout = optionValue;
+ break;
+ case SocketOptionName.Broadcast:
+ _broadcast = optionValue != 0;
+ break;
+ }
+
+ lock (_socketOptions)
+ {
+ _socketOptions[optionName] = optionValue;
+ }
+ }
+
+ public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue)
+ {
+ // Just linger uses this for now in BSD, which we ignore.
+ }
+
+ public void Shutdown(SocketShutdown how)
+ {
+ switch (how)
+ {
+ case SocketShutdown.Both:
+ _readShutdown = true;
+ // _writeShutdown = true;
+ break;
+ case SocketShutdown.Receive:
+ _readShutdown = true;
+ break;
+ case SocketShutdown.Send:
+ // _writeShutdown = true;
+ break;
+ }
+ }
+
+ public void ProxyDestroyed()
+ {
+ // Do nothing, for now. Will likely be more useful with TCP.
+ }
+
+ public void Dispose()
+ {
+
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs
new file mode 100644
index 000000000..7da1aa998
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs
@@ -0,0 +1,93 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
+using System.Net.Sockets;
+using System.Threading;
+using TcpClient = NetCoreServer.TcpClient;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
+{
+ class P2pProxyClient : TcpClient, IProxyClient
+ {
+ private const int FailureTimeout = 4000;
+
+ public ProxyConfig ProxyConfig { get; private set; }
+
+ private readonly RyuLdnProtocol _protocol;
+
+ private readonly ManualResetEvent _connected = new ManualResetEvent(false);
+ private readonly ManualResetEvent _ready = new ManualResetEvent(false);
+ private readonly AutoResetEvent _error = new AutoResetEvent(false);
+
+ public P2pProxyClient(string address, int port) : base(address, port)
+ {
+ if (ProxyHelpers.SupportsNoDelay())
+ {
+ OptionNoDelay = true;
+ }
+
+ _protocol = new RyuLdnProtocol();
+
+ _protocol.ProxyConfig += HandleProxyConfig;
+
+ ConnectAsync();
+ }
+
+ protected override void OnConnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client connected a new session with Id {Id}");
+
+ _connected.Set();
+ }
+
+ protected override void OnDisconnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client disconnected a session with Id {Id}");
+
+ SocketHelpers.UnregisterProxy();
+
+ _connected.Reset();
+ }
+
+ protected override void OnReceived(byte[] buffer, long offset, long size)
+ {
+ _protocol.Read(buffer, (int)offset, (int)size);
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP client caught an error with code {error}");
+
+ _error.Set();
+ }
+
+ private void HandleProxyConfig(LdnHeader header, ProxyConfig config)
+ {
+ ProxyConfig = config;
+
+ SocketHelpers.RegisterProxy(new LdnProxy(config, this, _protocol));
+
+ _ready.Set();
+ }
+
+ public bool EnsureProxyReady()
+ {
+ return _ready.WaitOne(FailureTimeout);
+ }
+
+ public bool PerformAuth(ExternalProxyConfig config)
+ {
+ bool signalled = _connected.WaitOne(FailureTimeout);
+
+ if (!signalled)
+ {
+ return false;
+ }
+
+ SendAsync(_protocol.Encode(PacketId.ExternalProxy, config));
+
+ return true;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs
new file mode 100644
index 000000000..598fb654f
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs
@@ -0,0 +1,388 @@
+using NetCoreServer;
+using Open.Nat;
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
+{
+ class P2pProxyServer : TcpServer, IDisposable
+ {
+ public const ushort PrivatePortBase = 39990;
+ public const int PrivatePortRange = 10;
+
+ private const ushort PublicPortBase = 39990;
+ private const int PublicPortRange = 10;
+
+ private const ushort PortLeaseLength = 60;
+ private const ushort PortLeaseRenew = 50;
+
+ private const ushort AuthWaitSeconds = 1;
+
+ private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
+
+ public ushort PrivatePort { get; }
+
+ private ushort _publicPort;
+
+ private bool _disposed;
+ private readonly CancellationTokenSource _disposedCancellation = new CancellationTokenSource();
+
+ private NatDevice _natDevice;
+ private Mapping _portMapping;
+
+ private readonly List _players = new List();
+
+ private readonly List _waitingTokens = new List();
+ private readonly AutoResetEvent _tokenEvent = new AutoResetEvent(false);
+
+ private uint _broadcastAddress;
+
+ private readonly LdnMasterProxyClient _master;
+ private readonly RyuLdnProtocol _masterProtocol;
+ private readonly RyuLdnProtocol _protocol;
+
+ public P2pProxyServer(LdnMasterProxyClient master, ushort port, RyuLdnProtocol masterProtocol) : base(IPAddress.Any, port)
+ {
+ if (ProxyHelpers.SupportsNoDelay())
+ {
+ OptionNoDelay = true;
+ }
+
+ PrivatePort = port;
+
+ _master = master;
+ _masterProtocol = masterProtocol;
+
+ _masterProtocol.ExternalProxyState += HandleStateChange;
+ _masterProtocol.ExternalProxyToken += HandleToken;
+
+ _protocol = new RyuLdnProtocol();
+ }
+
+ private void HandleToken(LdnHeader header, ExternalProxyToken token)
+ {
+ _lock.EnterWriteLock();
+
+ _waitingTokens.Add(token);
+
+ _lock.ExitWriteLock();
+
+ _tokenEvent.Set();
+ }
+
+ private void HandleStateChange(LdnHeader header, ExternalProxyConnectionState state)
+ {
+ if (!state.Connected)
+ {
+ _lock.EnterWriteLock();
+
+ _waitingTokens.RemoveAll(token => token.VirtualIp == state.IpAddress);
+
+ _players.RemoveAll(player =>
+ {
+ if (player.VirtualIpAddress == state.IpAddress)
+ {
+ player.DisconnectAndStop();
+
+ return true;
+ }
+
+ return false;
+ });
+
+ _lock.ExitWriteLock();
+ }
+ }
+
+ public void Configure(ProxyConfig config)
+ {
+ _broadcastAddress = config.ProxyIp | (~config.ProxySubnetMask);
+ }
+
+ public async Task NatPunch()
+ {
+ NatDiscoverer discoverer = new NatDiscoverer();
+ CancellationTokenSource cts = new CancellationTokenSource(1000);
+
+ NatDevice device;
+
+ try
+ {
+ device = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, cts);
+ }
+ catch (NatDeviceNotFoundException)
+ {
+ return 0;
+ }
+
+ _publicPort = PublicPortBase;
+
+ for (int i = 0; i < PublicPortRange; i++)
+ {
+ try
+ {
+ _portMapping = new Mapping(Protocol.Tcp, PrivatePort, _publicPort, PortLeaseLength, "Ryujinx Local Multiplayer");
+
+ await device.CreatePortMapAsync(_portMapping);
+
+ break;
+ }
+ catch (MappingException)
+ {
+ _publicPort++;
+ }
+ catch (Exception)
+ {
+ return 0;
+ }
+
+ if (i == PublicPortRange - 1)
+ {
+ _publicPort = 0;
+ }
+ }
+
+ if (_publicPort != 0)
+ {
+ _ = Task.Delay(PortLeaseRenew * 1000, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease));
+ }
+
+ _natDevice = device;
+
+ return _publicPort;
+ }
+
+ // Proxy handlers
+
+ private void RouteMessage(P2pProxySession sender, ref ProxyInfo info, Action action)
+ {
+ if (info.SourceIpV4 == 0)
+ {
+ // If they sent from a connection bound on 0.0.0.0, make others see it as them.
+ info.SourceIpV4 = sender.VirtualIpAddress;
+ }
+ else if (info.SourceIpV4 != sender.VirtualIpAddress)
+ {
+ // Can't pretend to be somebody else.
+ return;
+ }
+
+ uint destIp = info.DestIpV4;
+
+ if (destIp == 0xc0a800ff)
+ {
+ destIp = _broadcastAddress;
+ }
+
+ bool isBroadcast = destIp == _broadcastAddress;
+
+ _lock.EnterReadLock();
+
+ if (isBroadcast)
+ {
+ _players.ForEach(player =>
+ {
+ action(player);
+ });
+ }
+ else
+ {
+ P2pProxySession target = _players.FirstOrDefault(player => player.VirtualIpAddress == destIp);
+
+ if (target != null)
+ {
+ action(target);
+ }
+ }
+
+ _lock.ExitReadLock();
+ }
+
+ public void HandleProxyDisconnect(P2pProxySession sender, LdnHeader header, ProxyDisconnectMessage message)
+ {
+ RouteMessage(sender, ref message.Info, (target) =>
+ {
+ target.SendAsync(sender.Protocol.Encode(PacketId.ProxyDisconnect, message));
+ });
+ }
+
+ public void HandleProxyData(P2pProxySession sender, LdnHeader header, ProxyDataHeader message, byte[] data)
+ {
+ RouteMessage(sender, ref message.Info, (target) =>
+ {
+ target.SendAsync(sender.Protocol.Encode(PacketId.ProxyData, message, data));
+ });
+ }
+
+ public void HandleProxyConnectReply(P2pProxySession sender, LdnHeader header, ProxyConnectResponse message)
+ {
+ RouteMessage(sender, ref message.Info, (target) =>
+ {
+ target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnectReply, message));
+ });
+ }
+
+ public void HandleProxyConnect(P2pProxySession sender, LdnHeader header, ProxyConnectRequest message)
+ {
+ RouteMessage(sender, ref message.Info, (target) =>
+ {
+ target.SendAsync(sender.Protocol.Encode(PacketId.ProxyConnect, message));
+ });
+ }
+
+ // End proxy handlers
+
+ private async Task RefreshLease()
+ {
+ if (_disposed || _natDevice == null)
+ {
+ return;
+ }
+
+ try
+ {
+ await _natDevice.CreatePortMapAsync(_portMapping);
+ }
+ catch (Exception)
+ {
+
+ }
+
+ _ = Task.Delay(PortLeaseRenew, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease));
+ }
+
+ public bool TryRegisterUser(P2pProxySession session, ExternalProxyConfig config)
+ {
+ _lock.EnterWriteLock();
+
+ // Attempt to find matching configuration. If we don't find one, wait for a bit and try again.
+ // Woken by new tokens coming in from the master server.
+
+ IPAddress address = (session.Socket.RemoteEndPoint as IPEndPoint).Address;
+ byte[] addressBytes = ProxyHelpers.AddressTo16Byte(address);
+
+ long time;
+ long endTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency * AuthWaitSeconds;
+
+ do
+ {
+ for (int i = 0; i < _waitingTokens.Count; i++)
+ {
+ ExternalProxyToken waitToken = _waitingTokens[i];
+
+ // Allow any client that has a private IP to connect. (indicated by the server as all 0 in the token)
+
+ bool isPrivate = waitToken.PhysicalIp.AsSpan().SequenceEqual(new byte[16]);
+ bool ipEqual = isPrivate || waitToken.AddressFamily == address.AddressFamily && waitToken.PhysicalIp.AsSpan().SequenceEqual(addressBytes);
+
+ if (ipEqual && waitToken.Token.AsSpan().SequenceEqual(config.Token.AsSpan()))
+ {
+ // This is a match.
+
+ _waitingTokens.RemoveAt(i);
+
+ session.SetIpv4(waitToken.VirtualIp);
+
+ ProxyConfig pconfig = new ProxyConfig
+ {
+ ProxyIp = session.VirtualIpAddress,
+ ProxySubnetMask = 0xFFFF0000 // TODO: Use from server.
+ };
+
+ if (_players.Count == 0)
+ {
+ Configure(pconfig);
+ }
+
+ _players.Add(session);
+
+ session.SendAsync(_protocol.Encode(PacketId.ProxyConfig, pconfig));
+
+ _lock.ExitWriteLock();
+
+ return true;
+ }
+ }
+
+ // Couldn't find the token.
+ // It may not have arrived yet, so wait for one to arrive.
+
+ _lock.ExitWriteLock();
+
+ time = Stopwatch.GetTimestamp();
+ int remainingMs = (int)((endTime - time) / (Stopwatch.Frequency / 1000));
+
+ if (remainingMs < 0)
+ {
+ remainingMs = 0;
+ }
+
+ _tokenEvent.WaitOne(remainingMs);
+
+ _lock.EnterWriteLock();
+
+ } while (time < endTime);
+
+ _lock.ExitWriteLock();
+
+ return false;
+ }
+
+ public void DisconnectProxyClient(P2pProxySession session)
+ {
+ _lock.EnterWriteLock();
+
+ bool removed = _players.Remove(session);
+
+ if (removed)
+ {
+ _master.SendAsync(_masterProtocol.Encode(PacketId.ExternalProxyState, new ExternalProxyConnectionState
+ {
+ IpAddress = session.VirtualIpAddress,
+ Connected = false
+ }));
+ }
+
+ _lock.ExitWriteLock();
+ }
+
+ public new void Dispose()
+ {
+ base.Dispose();
+
+ _disposed = true;
+ _disposedCancellation.Cancel();
+
+ try
+ {
+ Task delete = _natDevice?.DeletePortMapAsync(new Mapping(Protocol.Tcp, PrivatePort, _publicPort, 60, "Ryujinx Local Multiplayer"));
+
+ // Just absorb any exceptions.
+ delete?.ContinueWith((task) => { });
+ }
+ catch (Exception)
+ {
+ // Fail silently.
+ }
+ }
+
+ protected override TcpSession CreateSession()
+ {
+ return new P2pProxySession(this);
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Proxy TCP server caught an error with code {error}");
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs
new file mode 100644
index 000000000..515feeac5
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs
@@ -0,0 +1,90 @@
+using NetCoreServer;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
+{
+ class P2pProxySession : TcpSession
+ {
+ public uint VirtualIpAddress { get; private set; }
+ public RyuLdnProtocol Protocol { get; }
+
+ private readonly P2pProxyServer _parent;
+
+ private bool _masterClosed;
+
+ public P2pProxySession(P2pProxyServer server) : base(server)
+ {
+ _parent = server;
+
+ Protocol = new RyuLdnProtocol();
+
+ Protocol.ProxyDisconnect += HandleProxyDisconnect;
+ Protocol.ProxyData += HandleProxyData;
+ Protocol.ProxyConnectReply += HandleProxyConnectReply;
+ Protocol.ProxyConnect += HandleProxyConnect;
+
+ Protocol.ExternalProxy += HandleAuthentication;
+ }
+
+ private void HandleAuthentication(LdnHeader header, ExternalProxyConfig token)
+ {
+ if (!_parent.TryRegisterUser(this, token))
+ {
+ Disconnect();
+ }
+ }
+
+ public void SetIpv4(uint ip)
+ {
+ VirtualIpAddress = ip;
+ }
+
+ public void DisconnectAndStop()
+ {
+ _masterClosed = true;
+
+ Disconnect();
+ }
+
+ protected override void OnDisconnected()
+ {
+ if (!_masterClosed)
+ {
+ _parent.DisconnectProxyClient(this);
+ }
+ }
+
+ protected override void OnReceived(byte[] buffer, long offset, long size)
+ {
+ try
+ {
+ Protocol.Read(buffer, (int)offset, (int)size);
+ }
+ catch (Exception)
+ {
+ Disconnect();
+ }
+ }
+
+ private void HandleProxyDisconnect(LdnHeader header, ProxyDisconnectMessage message)
+ {
+ _parent.HandleProxyDisconnect(this, header, message);
+ }
+
+ private void HandleProxyData(LdnHeader header, ProxyDataHeader message, byte[] data)
+ {
+ _parent.HandleProxyData(this, header, message, data);
+ }
+
+ private void HandleProxyConnectReply(LdnHeader header, ProxyConnectResponse data)
+ {
+ _parent.HandleProxyConnectReply(this, header, data);
+ }
+
+ private void HandleProxyConnect(LdnHeader header, ProxyConnectRequest message)
+ {
+ _parent.HandleProxyConnect(this, header, message);
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs
new file mode 100644
index 000000000..42b1ab6a2
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Net;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
+{
+ static class ProxyHelpers
+ {
+ public static byte[] AddressTo16Byte(IPAddress address)
+ {
+ byte[] ipBytes = new byte[16];
+ byte[] srcBytes = address.GetAddressBytes();
+
+ Array.Copy(srcBytes, 0, ipBytes, 0, srcBytes.Length);
+
+ return ipBytes;
+ }
+
+ public static bool SupportsNoDelay()
+ {
+ return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs
new file mode 100644
index 000000000..d0eeaf125
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs
@@ -0,0 +1,380 @@
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu
+{
+ class RyuLdnProtocol
+ {
+ private const byte CurrentProtocolVersion = 1;
+ private const int Magic = ('R' << 0) | ('L' << 8) | ('D' << 16) | ('N' << 24);
+ private const int MaxPacketSize = 131072;
+
+ private readonly int _headerSize = Marshal.SizeOf();
+
+ private readonly byte[] _buffer = new byte[MaxPacketSize];
+ private int _bufferEnd = 0;
+
+ // Client Packets.
+ public event Action Initialize;
+ public event Action Passphrase;
+ public event Action Connected;
+ public event Action SyncNetwork;
+ public event Action ScanReply;
+ public event Action ScanReplyEnd;
+ public event Action Disconnected;
+
+ // External Proxy Packets.
+ public event Action ExternalProxy;
+ public event Action ExternalProxyState;
+ public event Action ExternalProxyToken;
+
+ // Server Packets.
+ public event Action CreateAccessPoint;
+ public event Action CreateAccessPointPrivate;
+ public event Action Reject;
+ public event Action RejectReply;
+ public event Action SetAcceptPolicy;
+ public event Action SetAdvertiseData;
+ public event Action Connect;
+ public event Action ConnectPrivate;
+ public event Action Scan;
+
+ // Proxy Packets.
+ public event Action ProxyConfig;
+ public event Action ProxyConnect;
+ public event Action ProxyConnectReply;
+ public event Action ProxyData;
+ public event Action ProxyDisconnect;
+
+ // Lifecycle Packets.
+ public event Action NetworkError;
+ public event Action Ping;
+
+ public RyuLdnProtocol() { }
+
+ public void Reset()
+ {
+ _bufferEnd = 0;
+ }
+
+ public void Read(byte[] data, int offset, int size)
+ {
+ int index = 0;
+
+ while (index < size)
+ {
+ if (_bufferEnd < _headerSize)
+ {
+ // Assemble the header first.
+
+ int copyable = Math.Min(size - index, Math.Min(size, _headerSize - _bufferEnd));
+
+ Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable);
+
+ index += copyable;
+ _bufferEnd += copyable;
+ }
+
+ if (_bufferEnd >= _headerSize)
+ {
+ // The header is available. Make sure we received all the data (size specified in the header)
+
+ LdnHeader ldnHeader = MemoryMarshal.Cast(_buffer)[0];
+
+ if (ldnHeader.Magic != Magic)
+ {
+ throw new InvalidOperationException("Invalid magic number in received packet.");
+ }
+
+ if (ldnHeader.Version != CurrentProtocolVersion)
+ {
+ throw new InvalidOperationException($"Protocol version mismatch. Expected ${CurrentProtocolVersion}, was ${ldnHeader.Version}.");
+ }
+
+ int finalSize = _headerSize + ldnHeader.DataSize;
+
+ if (finalSize >= MaxPacketSize)
+ {
+ throw new InvalidOperationException($"Max packet size {MaxPacketSize} exceeded.");
+ }
+
+ int copyable = Math.Min(size - index, Math.Min(size, finalSize - _bufferEnd));
+
+ Array.Copy(data, index + offset, _buffer, _bufferEnd, copyable);
+
+ index += copyable;
+ _bufferEnd += copyable;
+
+ if (finalSize == _bufferEnd)
+ {
+ // The full packet has been retrieved. Send it to be decoded.
+
+ byte[] ldnData = new byte[ldnHeader.DataSize];
+
+ Array.Copy(_buffer, _headerSize, ldnData, 0, ldnData.Length);
+
+ DecodeAndHandle(ldnHeader, ldnData);
+
+ Reset();
+ }
+ }
+ }
+ }
+
+ private (T, byte[]) ParseWithData(byte[] data) where T : struct
+ {
+ T str = default;
+ int size = Marshal.SizeOf(str);
+
+ byte[] remainder = new byte[data.Length - size];
+
+ if (remainder.Length > 0)
+ {
+ Array.Copy(data, size, remainder, 0, remainder.Length);
+ }
+
+ return (MemoryMarshal.Read(data), remainder);
+ }
+
+ private void DecodeAndHandle(LdnHeader header, byte[] data)
+ {
+ switch ((PacketId)header.Type)
+ {
+ // Client Packets.
+ case PacketId.Initialize:
+ {
+ Initialize?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.Passphrase:
+ {
+ Passphrase?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.Connected:
+ {
+ Connected?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.SyncNetwork:
+ {
+ SyncNetwork?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.ScanReply:
+ {
+ ScanReply?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+
+ case PacketId.ScanReplyEnd:
+ {
+ ScanReplyEnd?.Invoke(header);
+
+ break;
+ }
+ case PacketId.Disconnect:
+ {
+ Disconnected?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+
+ // External Proxy Packets.
+ case PacketId.ExternalProxy:
+ {
+ ExternalProxy?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.ExternalProxyState:
+ {
+ ExternalProxyState?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.ExternalProxyToken:
+ {
+ ExternalProxyToken?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+
+ // Server Packets.
+ case PacketId.CreateAccessPoint:
+ {
+ (CreateAccessPointRequest packet, byte[] extraData) = ParseWithData(data);
+ CreateAccessPoint?.Invoke(header, packet, extraData);
+ break;
+ }
+ case PacketId.CreateAccessPointPrivate:
+ {
+ (CreateAccessPointPrivateRequest packet, byte[] extraData) = ParseWithData(data);
+ CreateAccessPointPrivate?.Invoke(header, packet, extraData);
+ break;
+ }
+ case PacketId.Reject:
+ {
+ Reject?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.RejectReply:
+ {
+ RejectReply?.Invoke(header);
+
+ break;
+ }
+ case PacketId.SetAcceptPolicy:
+ {
+ SetAcceptPolicy?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.SetAdvertiseData:
+ {
+ SetAdvertiseData?.Invoke(header, data);
+
+ break;
+ }
+ case PacketId.Connect:
+ {
+ Connect?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.ConnectPrivate:
+ {
+ ConnectPrivate?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.Scan:
+ {
+ Scan?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+
+ // Proxy Packets
+ case PacketId.ProxyConfig:
+ {
+ ProxyConfig?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.ProxyConnect:
+ {
+ ProxyConnect?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.ProxyConnectReply:
+ {
+ ProxyConnectReply?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.ProxyData:
+ {
+ (ProxyDataHeader packet, byte[] extraData) = ParseWithData(data);
+
+ ProxyData?.Invoke(header, packet, extraData);
+
+ break;
+ }
+ case PacketId.ProxyDisconnect:
+ {
+ ProxyDisconnect?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+
+ // Lifecycle Packets.
+ case PacketId.Ping:
+ {
+ Ping?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+ case PacketId.NetworkError:
+ {
+ NetworkError?.Invoke(header, MemoryMarshal.Read(data));
+
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+
+ private static LdnHeader GetHeader(PacketId type, int dataSize)
+ {
+ return new LdnHeader()
+ {
+ Magic = Magic,
+ Version = CurrentProtocolVersion,
+ Type = (byte)type,
+ DataSize = dataSize
+ };
+ }
+
+ public byte[] Encode(PacketId type)
+ {
+ LdnHeader header = GetHeader(type, 0);
+
+ return SpanHelpers.AsSpan(ref header).ToArray();
+ }
+
+ public byte[] Encode(PacketId type, byte[] data)
+ {
+ LdnHeader header = GetHeader(type, data.Length);
+
+ byte[] result = SpanHelpers.AsSpan(ref header).ToArray();
+
+ Array.Resize(ref result, result.Length + data.Length);
+ Array.Copy(data, 0, result, Marshal.SizeOf(), data.Length);
+
+ return result;
+ }
+
+ public byte[] Encode(PacketId type, T packet) where T : unmanaged
+ {
+ byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray();
+
+ LdnHeader header = GetHeader(type, packetData.Length);
+
+ byte[] result = SpanHelpers.AsSpan(ref header).ToArray();
+
+ Array.Resize(ref result, result.Length + packetData.Length);
+ Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length);
+
+ return result;
+ }
+
+ public byte[] Encode(PacketId type, T packet, byte[] data) where T : unmanaged
+ {
+ byte[] packetData = SpanHelpers.AsSpan(ref packet).ToArray();
+
+ LdnHeader header = GetHeader(type, packetData.Length + data.Length);
+
+ byte[] result = SpanHelpers.AsSpan(ref header).ToArray();
+
+ Array.Resize(ref result, result.Length + packetData.Length + data.Length);
+ Array.Copy(packetData, 0, result, Marshal.SizeOf(), packetData.Length);
+ Array.Copy(data, 0, result, Marshal.SizeOf() + packetData.Length, data.Length);
+
+ return result;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs
new file mode 100644
index 000000000..448d33f29
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs
@@ -0,0 +1,10 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x4)]
+ struct DisconnectMessage
+ {
+ public uint DisconnectIP;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs
new file mode 100644
index 000000000..9cbb80242
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs
@@ -0,0 +1,19 @@
+using Ryujinx.Common.Memory;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ ///
+ /// Sent by the server to point a client towards an external server being used as a proxy.
+ /// The client then forwards this to the external proxy after connecting, to verify the connection worked.
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0x26, Pack = 1)]
+ struct ExternalProxyConfig
+ {
+ public Array16 ProxyIp;
+ public AddressFamily AddressFamily;
+ public ushort ProxyPort;
+ public Array16 Token;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs
new file mode 100644
index 000000000..ecf4e14f7
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs
@@ -0,0 +1,18 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ ///
+ /// Indicates a change in connection state for the given client.
+ /// Is sent to notify the master server when connection is first established.
+ /// Can be sent by the external proxy to the master server to notify it of a proxy disconnect.
+ /// Can be sent by the master server to notify the external proxy of a user leaving a room.
+ /// Both will result in a force kick.
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0x8, Pack = 4)]
+ struct ExternalProxyConnectionState
+ {
+ public uint IpAddress;
+ public bool Connected;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs
new file mode 100644
index 000000000..0a8980c37
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs
@@ -0,0 +1,20 @@
+using Ryujinx.Common.Memory;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ ///
+ /// Sent by the master server to an external proxy to tell them someone is going to connect.
+ /// This drives authentication, and lets the proxy know what virtual IP to give to each joiner,
+ /// as these are managed by the master server.
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0x28)]
+ struct ExternalProxyToken
+ {
+ public uint VirtualIp;
+ public Array16 Token;
+ public Array16 PhysicalIp;
+ public AddressFamily AddressFamily;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs
new file mode 100644
index 000000000..36ddc65fe
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs
@@ -0,0 +1,20 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ ///
+ /// This message is first sent by the client to identify themselves.
+ /// If the server has a token+mac combo that matches the submission, then they are returned their new ID and mac address. (the mac is also reassigned to the new id)
+ /// Otherwise, they are returned a random mac address.
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0x16)]
+ struct InitializeMessage
+ {
+ // All 0 if we don't have an ID yet.
+ public Array16 Id;
+
+ // All 0 if we don't have a mac yet.
+ public Array6 MacAddress;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs
new file mode 100644
index 000000000..f41f15ab4
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs
@@ -0,0 +1,13 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0xA)]
+ struct LdnHeader
+ {
+ public uint Magic;
+ public byte Type;
+ public byte Version;
+ public int DataSize;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs
new file mode 100644
index 000000000..b8ef5fbc1
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs
@@ -0,0 +1,36 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ enum PacketId
+ {
+ Initialize,
+ Passphrase,
+
+ CreateAccessPoint,
+ CreateAccessPointPrivate,
+ ExternalProxy,
+ ExternalProxyToken,
+ ExternalProxyState,
+ SyncNetwork,
+ Reject,
+ RejectReply,
+ Scan,
+ ScanReply,
+ ScanReplyEnd,
+ Connect,
+ ConnectPrivate,
+ Connected,
+ Disconnect,
+
+ ProxyConfig,
+ ProxyConnect,
+ ProxyConnectReply,
+ ProxyData,
+ ProxyDisconnect,
+
+ SetAcceptPolicy,
+ SetAdvertiseData,
+
+ Ping = 254,
+ NetworkError = 255
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs
new file mode 100644
index 000000000..0deba0b07
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs
@@ -0,0 +1,11 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x80)]
+ struct PassphraseMessage
+ {
+ public Array128 Passphrase;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs
new file mode 100644
index 000000000..135e39caa
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs
@@ -0,0 +1,11 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x2)]
+ struct PingMessage
+ {
+ public byte Requester;
+ public byte Id;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs
new file mode 100644
index 000000000..ffce77791
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs
@@ -0,0 +1,10 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x10)]
+ struct ProxyConnectRequest
+ {
+ public ProxyInfo Info;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs
new file mode 100644
index 000000000..de2e430fb
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs
@@ -0,0 +1,10 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x10)]
+ struct ProxyConnectResponse
+ {
+ public ProxyInfo Info;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs
new file mode 100644
index 000000000..e46a40692
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs
@@ -0,0 +1,14 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ ///
+ /// Represents data sent over a transport layer.
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0x14)]
+ struct ProxyDataHeader
+ {
+ public ProxyInfo Info;
+ public uint DataLength; // Followed by the data with the specified byte length.
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs
new file mode 100644
index 000000000..eb3648413
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ class ProxyDataPacket
+ {
+ public ProxyDataHeader Header;
+ public byte[] Data;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs
new file mode 100644
index 000000000..2154ae109
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs
@@ -0,0 +1,11 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x14)]
+ struct ProxyDisconnectMessage
+ {
+ public ProxyInfo Info;
+ public int DisconnectReason;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs
new file mode 100644
index 000000000..d9338f244
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs
@@ -0,0 +1,20 @@
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ ///
+ /// Information included in all proxied communication.
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0x10, Pack = 1)]
+ struct ProxyInfo
+ {
+ public uint SourceIpV4;
+ public ushort SourcePort;
+
+ public uint DestIpV4;
+ public ushort DestPort;
+
+ public ProtocolType Protocol;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs
new file mode 100644
index 000000000..1c2ce1f8b
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs
@@ -0,0 +1,18 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x8)]
+ struct RejectRequest
+ {
+ public uint NodeId;
+ public DisconnectReason DisconnectReason;
+
+ public RejectRequest(DisconnectReason disconnectReason, uint nodeId)
+ {
+ DisconnectReason = disconnectReason;
+ NodeId = nodeId;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs
new file mode 100644
index 000000000..f3bd72023
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs
@@ -0,0 +1,23 @@
+using Ryujinx.Common.Memory;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x28, Pack = 1)]
+ struct RyuNetworkConfig
+ {
+ public Array16 GameVersion;
+
+ // PrivateIp is included for external proxies for the case where a client attempts to join from
+ // their own LAN. UPnP forwarding can fail when connecting devices on the same network over the public IP,
+ // so if their public IP is identical, the internal address should be sent instead.
+
+ // The fields below are 0 if not hosting a p2p proxy.
+
+ public Array16 PrivateIp;
+ public AddressFamily AddressFamily;
+ public ushort ExternalProxyPort;
+ public ushort InternalProxyPort;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs
new file mode 100644
index 000000000..c4a969901
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs
@@ -0,0 +1,11 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x1, Pack = 1)]
+ struct SetAcceptPolicyRequest
+ {
+ public AcceptPolicy StationAcceptPolicy;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
index e39c01978..fa43f789e 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
@@ -14,6 +14,8 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public bool Connected { get; private set; }
+ public ProxyConfig Config => _parent.NetworkClient.Config;
+
public Station(IUserLocalCommunicationService parent)
{
_parent = parent;
@@ -48,9 +50,12 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
public void Dispose()
{
- _parent.NetworkClient.DisconnectNetwork();
+ if (_parent.NetworkClient != null)
+ {
+ _parent.NetworkClient.DisconnectNetwork();
- _parent.NetworkClient.NetworkChange -= NetworkChanged;
+ _parent.NetworkClient.NetworkChange -= NetworkChanged;
+ }
}
private ResultCode NetworkErrorToResult(NetworkError error)
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
index ac0ff7d94..0972c21c0 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
@@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
@@ -14,5 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
public UserConfig UserConfig;
public NetworkConfig NetworkConfig;
public AddressList AddressList;
+
+ public RyuNetworkConfig RyuNetworkConfig;
}
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
index f67f0aac9..d2dc5b698 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
@@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Types;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
@@ -6,11 +7,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
///
/// Advertise data is appended separately (remaining data in the buffer).
///
- [StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)]
+ [StructLayout(LayoutKind.Sequential, Size = 0xBC, Pack = 1)]
struct CreateAccessPointRequest
{
public SecurityConfig SecurityConfig;
public UserConfig UserConfig;
public NetworkConfig NetworkConfig;
+
+ public RyuNetworkConfig RyuNetworkConfig;
}
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs
new file mode 100644
index 000000000..c89c08bbe
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs
@@ -0,0 +1,11 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x8)]
+ struct ProxyConfig
+ {
+ public uint ProxyIp;
+ public uint ProxySubnetMask;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs
index 21d48288e..3a40a4ac5 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/IClient.cs
@@ -95,7 +95,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd
}
}
- ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol)
+ ISocket newBsdSocket = new ManagedSocket(netDomain, (SocketType)type, protocol, context.Device.Configuration.MultiplayerLanInterfaceId)
{
Blocking = !creationFlags.HasFlag(BsdSocketCreationFlags.NonBlocking),
};
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs
index c9b811cf5..981fe0a8f 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs
@@ -1,4 +1,5 @@
using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
using System;
using System.Collections.Generic;
@@ -21,21 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; }
- public nint Handle => Socket.Handle;
+ public nint Handle => IntPtr.Zero;
public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;
public IPEndPoint LocalEndPoint => Socket.LocalEndPoint as IPEndPoint;
- public Socket Socket { get; }
+ public ISocketImpl Socket { get; }
- public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType)
+ public ManagedSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType, string lanInterfaceId)
{
- Socket = new Socket(addressFamily, socketType, protocolType);
+ Socket = SocketHelpers.CreateSocket(addressFamily, socketType, protocolType, lanInterfaceId);
Refcount = 1;
}
- private ManagedSocket(Socket socket)
+ private ManagedSocket(ISocketImpl socket)
{
Socket = socket;
Refcount = 1;
@@ -185,6 +186,8 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
}
}
+ bool hasEmittedBlockingWarning = false;
+
public LinuxError Receive(out int receiveSize, Span buffer, BsdSocketFlags flags)
{
LinuxError result;
@@ -199,6 +202,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
shouldBlockAfterOperation = true;
}
+ if (Blocking && !hasEmittedBlockingWarning)
+ {
+ Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors.");
+ hasEmittedBlockingWarning = true;
+ }
+
receiveSize = Socket.Receive(buffer, ConvertBsdSocketFlags(flags));
result = LinuxError.SUCCESS;
@@ -236,6 +245,12 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
shouldBlockAfterOperation = true;
}
+ if (Blocking && !hasEmittedBlockingWarning)
+ {
+ Logger.Warning?.PrintMsg(LogClass.ServiceBsd, "Blocking socket operations are not yet working properly. Expect network errors.");
+ hasEmittedBlockingWarning = true;
+ }
+
if (!Socket.IsBound)
{
receiveSize = -1;
@@ -313,7 +328,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported GetSockOpt Option: {option} Level: {level}");
optionValue.Clear();
- return LinuxError.SUCCESS;
+ return LinuxError.EOPNOTSUPP;
}
byte[] tempOptionValue = new byte[optionValue.Length];
@@ -347,7 +362,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{
Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported SetSockOpt Option: {option} Level: {level}");
- return LinuxError.SUCCESS;
+ return LinuxError.EOPNOTSUPP;
}
int value = optionValue.Length >= 4 ? MemoryMarshal.Read(optionValue) : MemoryMarshal.Read(optionValue);
@@ -493,7 +508,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
try
{
- int receiveSize = Socket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
+ int receiveSize = (Socket as DefaultSocket).BaseSocket.Receive(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
if (receiveSize > 0)
{
@@ -531,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
try
{
- int sendSize = Socket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
+ int sendSize = (Socket as DefaultSocket).BaseSocket.Send(ConvertMessagesToBuffer(message), ConvertBsdSocketFlags(flags), out SocketError socketError);
if (sendSize > 0)
{
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs
index d0db44086..e870e8aea 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocketPollManager.cs
@@ -1,4 +1,5 @@
using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Types;
using System.Collections.Generic;
using System.Net.Sockets;
@@ -26,45 +27,46 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public LinuxError Poll(List events, int timeoutMilliseconds, out int updatedCount)
{
- List readEvents = new();
- List writeEvents = new();
- List errorEvents = new();
+ List readEvents = new();
+ List writeEvents = new();
+ List errorEvents = new();
updatedCount = 0;
foreach (PollEvent evnt in events)
{
- ManagedSocket socket = (ManagedSocket)evnt.FileDescriptor;
-
- bool isValidEvent = evnt.Data.InputEvents == 0;
-
- errorEvents.Add(socket.Socket);
-
- if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
+ if (evnt.FileDescriptor is ManagedSocket ms)
{
- readEvents.Add(socket.Socket);
+ bool isValidEvent = evnt.Data.InputEvents == 0;
- isValidEvent = true;
- }
+ errorEvents.Add(ms.Socket);
- if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0)
- {
- readEvents.Add(socket.Socket);
+ if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
+ {
+ readEvents.Add(ms.Socket);
- isValidEvent = true;
- }
+ isValidEvent = true;
+ }
- if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0)
- {
- writeEvents.Add(socket.Socket);
+ if ((evnt.Data.InputEvents & PollEventTypeMask.UrgentInput) != 0)
+ {
+ readEvents.Add(ms.Socket);
- isValidEvent = true;
- }
+ isValidEvent = true;
+ }
- if (!isValidEvent)
- {
- Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}");
- return LinuxError.EINVAL;
+ if ((evnt.Data.InputEvents & PollEventTypeMask.Output) != 0)
+ {
+ writeEvents.Add(ms.Socket);
+
+ isValidEvent = true;
+ }
+
+ if (!isValidEvent)
+ {
+ Logger.Warning?.Print(LogClass.ServiceBsd, $"Unsupported Poll input event type: {evnt.Data.InputEvents}");
+ return LinuxError.EINVAL;
+ }
}
}
@@ -72,7 +74,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
{
int actualTimeoutMicroseconds = timeoutMilliseconds == -1 ? -1 : timeoutMilliseconds * 1000;
- Socket.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds);
+ SocketHelpers.Select(readEvents, writeEvents, errorEvents, actualTimeoutMicroseconds);
}
catch (SocketException exception)
{
@@ -81,34 +83,37 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
foreach (PollEvent evnt in events)
{
- Socket socket = ((ManagedSocket)evnt.FileDescriptor).Socket;
-
- PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents;
-
- if (errorEvents.Contains(socket))
+ if (evnt.FileDescriptor is ManagedSocket ms)
{
- outputEvents |= PollEventTypeMask.Error;
+ ISocketImpl socket = ms.Socket;
- if (!socket.Connected || !socket.IsBound)
+ PollEventTypeMask outputEvents = evnt.Data.OutputEvents & ~evnt.Data.InputEvents;
+
+ if (errorEvents.Contains(ms.Socket))
{
- outputEvents |= PollEventTypeMask.Disconnected;
- }
- }
+ outputEvents |= PollEventTypeMask.Error;
- if (readEvents.Contains(socket))
- {
- if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
+ if (!socket.Connected || !socket.IsBound)
+ {
+ outputEvents |= PollEventTypeMask.Disconnected;
+ }
+ }
+
+ if (readEvents.Contains(ms.Socket))
{
- outputEvents |= PollEventTypeMask.Input;
+ if ((evnt.Data.InputEvents & PollEventTypeMask.Input) != 0)
+ {
+ outputEvents |= PollEventTypeMask.Input;
+ }
}
- }
- if (writeEvents.Contains(socket))
- {
- outputEvents |= PollEventTypeMask.Output;
- }
+ if (writeEvents.Contains(ms.Socket))
+ {
+ outputEvents |= PollEventTypeMask.Output;
+ }
- evnt.Data.OutputEvents = outputEvents;
+ evnt.Data.OutputEvents = outputEvents;
+ }
}
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
@@ -118,53 +123,55 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
public LinuxError Select(List events, int timeout, out int updatedCount)
{
- List readEvents = new();
- List writeEvents = new();
- List errorEvents = new();
+ List readEvents = new();
+ List writeEvents = new();
+ List errorEvents = new();
updatedCount = 0;
foreach (PollEvent pollEvent in events)
{
- ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor;
-
- if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input))
+ if (pollEvent.FileDescriptor is ManagedSocket ms)
{
- readEvents.Add(socket.Socket);
- }
+ if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Input))
+ {
+ readEvents.Add(ms.Socket);
+ }
- if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output))
- {
- writeEvents.Add(socket.Socket);
- }
+ if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Output))
+ {
+ writeEvents.Add(ms.Socket);
+ }
- if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error))
- {
- errorEvents.Add(socket.Socket);
+ if (pollEvent.Data.InputEvents.HasFlag(PollEventTypeMask.Error))
+ {
+ errorEvents.Add(ms.Socket);
+ }
}
}
- Socket.Select(readEvents, writeEvents, errorEvents, timeout);
+ SocketHelpers.Select(readEvents, writeEvents, errorEvents, timeout);
updatedCount = readEvents.Count + writeEvents.Count + errorEvents.Count;
foreach (PollEvent pollEvent in events)
{
- ManagedSocket socket = (ManagedSocket)pollEvent.FileDescriptor;
-
- if (readEvents.Contains(socket.Socket))
+ if (pollEvent.FileDescriptor is ManagedSocket ms)
{
- pollEvent.Data.OutputEvents |= PollEventTypeMask.Input;
- }
+ if (readEvents.Contains(ms.Socket))
+ {
+ pollEvent.Data.OutputEvents |= PollEventTypeMask.Input;
+ }
- if (writeEvents.Contains(socket.Socket))
- {
- pollEvent.Data.OutputEvents |= PollEventTypeMask.Output;
- }
+ if (writeEvents.Contains(ms.Socket))
+ {
+ pollEvent.Data.OutputEvents |= PollEventTypeMask.Output;
+ }
- if (errorEvents.Contains(socket.Socket))
- {
- pollEvent.Data.OutputEvents |= PollEventTypeMask.Error;
+ if (errorEvents.Contains(ms.Socket))
+ {
+ pollEvent.Data.OutputEvents |= PollEventTypeMask.Error;
+ }
}
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs
new file mode 100644
index 000000000..f1040e799
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs
@@ -0,0 +1,178 @@
+using Ryujinx.Common.Utilities;
+using System;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy
+{
+ class DefaultSocket : ISocketImpl
+ {
+ public Socket BaseSocket { get; }
+
+ public EndPoint RemoteEndPoint => BaseSocket.RemoteEndPoint;
+
+ public EndPoint LocalEndPoint => BaseSocket.LocalEndPoint;
+
+ public bool Connected => BaseSocket.Connected;
+
+ public bool IsBound => BaseSocket.IsBound;
+
+ public AddressFamily AddressFamily => BaseSocket.AddressFamily;
+
+ public SocketType SocketType => BaseSocket.SocketType;
+
+ public ProtocolType ProtocolType => BaseSocket.ProtocolType;
+
+ public bool Blocking { get => BaseSocket.Blocking; set => BaseSocket.Blocking = value; }
+
+ public int Available => BaseSocket.Available;
+
+ private readonly string _lanInterfaceId;
+
+ public DefaultSocket(Socket baseSocket, string lanInterfaceId)
+ {
+ _lanInterfaceId = lanInterfaceId;
+
+ BaseSocket = baseSocket;
+ }
+
+ public DefaultSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId)
+ {
+ _lanInterfaceId = lanInterfaceId;
+
+ BaseSocket = new Socket(domain, type, protocol);
+ }
+
+ private void EnsureNetworkInterfaceBound()
+ {
+ if (_lanInterfaceId != "0" && !BaseSocket.IsBound)
+ {
+ (_, UnicastIPAddressInformation ipInfo) = NetworkHelpers.GetLocalInterface(_lanInterfaceId);
+
+ BaseSocket.Bind(new IPEndPoint(ipInfo.Address, 0));
+ }
+ }
+
+ public ISocketImpl Accept()
+ {
+ return new DefaultSocket(BaseSocket.Accept(), _lanInterfaceId);
+ }
+
+ public void Bind(EndPoint localEP)
+ {
+ // NOTE: The guest is able to receive on 0.0.0.0 without it being limited to the chosen network interface.
+ // This is because it must get loopback traffic as well. This could allow other network traffic to leak in.
+
+ BaseSocket.Bind(localEP);
+ }
+
+ public void Close()
+ {
+ BaseSocket.Close();
+ }
+
+ public void Connect(EndPoint remoteEP)
+ {
+ EnsureNetworkInterfaceBound();
+
+ BaseSocket.Connect(remoteEP);
+ }
+
+ public void Disconnect(bool reuseSocket)
+ {
+ BaseSocket.Disconnect(reuseSocket);
+ }
+
+ public void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue)
+ {
+ BaseSocket.GetSocketOption(optionLevel, optionName, optionValue);
+ }
+
+ public void Listen(int backlog)
+ {
+ BaseSocket.Listen(backlog);
+ }
+
+ public int Receive(Span buffer)
+ {
+ EnsureNetworkInterfaceBound();
+
+ return BaseSocket.Receive(buffer);
+ }
+
+ public int Receive(Span buffer, SocketFlags flags)
+ {
+ EnsureNetworkInterfaceBound();
+
+ return BaseSocket.Receive(buffer, flags);
+ }
+
+ public int Receive(Span buffer, SocketFlags flags, out SocketError socketError)
+ {
+ EnsureNetworkInterfaceBound();
+
+ return BaseSocket.Receive(buffer, flags, out socketError);
+ }
+
+ public int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP)
+ {
+ EnsureNetworkInterfaceBound();
+
+ return BaseSocket.ReceiveFrom(buffer, flags, ref remoteEP);
+ }
+
+ public int Send(ReadOnlySpan buffer)
+ {
+ EnsureNetworkInterfaceBound();
+
+ return BaseSocket.Send(buffer);
+ }
+
+ public int Send(ReadOnlySpan buffer, SocketFlags flags)
+ {
+ EnsureNetworkInterfaceBound();
+
+ return BaseSocket.Send(buffer, flags);
+ }
+
+ public int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError)
+ {
+ EnsureNetworkInterfaceBound();
+
+ return BaseSocket.Send(buffer, flags, out socketError);
+ }
+
+ public int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP)
+ {
+ EnsureNetworkInterfaceBound();
+
+ return BaseSocket.SendTo(buffer, flags, remoteEP);
+ }
+
+ public bool Poll(int microSeconds, SelectMode mode)
+ {
+ return BaseSocket.Poll(microSeconds, mode);
+ }
+
+ public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue)
+ {
+ BaseSocket.SetSocketOption(optionLevel, optionName, optionValue);
+ }
+
+ public void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue)
+ {
+ BaseSocket.SetSocketOption(optionLevel, optionName, optionValue);
+ }
+
+ public void Shutdown(SocketShutdown how)
+ {
+ BaseSocket.Shutdown(how);
+ }
+
+ public void Dispose()
+ {
+ BaseSocket.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs
new file mode 100644
index 000000000..b7055f08b
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy
+{
+ interface ISocketImpl : IDisposable
+ {
+ EndPoint RemoteEndPoint { get; }
+ EndPoint LocalEndPoint { get; }
+ bool Connected { get; }
+ bool IsBound { get; }
+
+ AddressFamily AddressFamily { get; }
+ SocketType SocketType { get; }
+ ProtocolType ProtocolType { get; }
+
+ bool Blocking { get; set; }
+ int Available { get; }
+
+ int Receive(Span buffer);
+ int Receive(Span buffer, SocketFlags flags);
+ int Receive(Span buffer, SocketFlags flags, out SocketError socketError);
+ int ReceiveFrom(Span buffer, SocketFlags flags, ref EndPoint remoteEP);
+
+ int Send(ReadOnlySpan buffer);
+ int Send(ReadOnlySpan buffer, SocketFlags flags);
+ int Send(ReadOnlySpan buffer, SocketFlags flags, out SocketError socketError);
+ int SendTo(ReadOnlySpan buffer, SocketFlags flags, EndPoint remoteEP);
+
+ bool Poll(int microSeconds, SelectMode mode);
+
+ ISocketImpl Accept();
+
+ void Bind(EndPoint localEP);
+ void Connect(EndPoint remoteEP);
+ void Listen(int backlog);
+
+ void GetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, byte[] optionValue);
+ void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, int optionValue);
+ void SetSocketOption(SocketOptionLevel optionLevel, SocketOptionName optionName, object optionValue);
+
+ void Shutdown(SocketShutdown how);
+ void Disconnect(bool reuseSocket);
+ void Close();
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs
new file mode 100644
index 000000000..485a7f86b
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs
@@ -0,0 +1,74 @@
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy
+{
+ static class SocketHelpers
+ {
+ private static LdnProxy _proxy;
+
+ public static void Select(List readEvents, List writeEvents, List errorEvents, int timeout)
+ {
+ var readDefault = readEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList();
+ var writeDefault = writeEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList();
+ var errorDefault = errorEvents.Select(x => (x as DefaultSocket)?.BaseSocket).Where(x => x != null).ToList();
+
+ if (readDefault.Count != 0 || writeDefault.Count != 0 || errorDefault.Count != 0)
+ {
+ Socket.Select(readDefault, writeDefault, errorDefault, timeout);
+ }
+
+ void FilterSockets(List removeFrom, List selectedSockets, Func ldnCheck)
+ {
+ removeFrom.RemoveAll(socket =>
+ {
+ switch (socket)
+ {
+ case DefaultSocket dsocket:
+ return !selectedSockets.Contains(dsocket.BaseSocket);
+ case LdnProxySocket psocket:
+ return !ldnCheck(psocket);
+ default:
+ throw new NotImplementedException();
+ }
+ });
+ };
+
+ FilterSockets(readEvents, readDefault, (socket) => socket.Readable);
+ FilterSockets(writeEvents, writeDefault, (socket) => socket.Writable);
+ FilterSockets(errorEvents, errorDefault, (socket) => socket.Error);
+ }
+
+ public static void RegisterProxy(LdnProxy proxy)
+ {
+ if (_proxy != null)
+ {
+ UnregisterProxy();
+ }
+
+ _proxy = proxy;
+ }
+
+ public static void UnregisterProxy()
+ {
+ _proxy?.Dispose();
+ _proxy = null;
+ }
+
+ public static ISocketImpl CreateSocket(AddressFamily domain, SocketType type, ProtocolType protocol, string lanInterfaceId)
+ {
+ if (_proxy != null)
+ {
+ if (_proxy.Supported(domain, type, protocol))
+ {
+ return new LdnProxySocket(domain, type, protocol, _proxy);
+ }
+ }
+
+ return new DefaultSocket(domain, type, protocol, lanInterfaceId);
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs
index 39af90383..5b2de13f0 100644
--- a/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs
@@ -292,7 +292,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Sfdnsres
{
string host = MemoryHelper.ReadAsciiString(context.Memory, inputBufferPosition, (int)inputBufferSize);
- if (!context.Device.Configuration.EnableInternetAccess)
+ if (host != "localhost" && !context.Device.Configuration.EnableInternetAccess)
{
Logger.Info?.Print(LogClass.ServiceSfdnsres, $"Guest network access disabled, DNS Blocked: {host}");
diff --git a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs
index 8cc761baf..dc33dd6a5 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ssl/SslService/SslManagedSocketConnection.cs
@@ -1,5 +1,6 @@
using Ryujinx.HLE.HOS.Services.Sockets.Bsd;
using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl;
+using Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy;
using Ryujinx.HLE.HOS.Services.Ssl.Types;
using System;
using System.IO;
@@ -116,7 +117,7 @@ namespace Ryujinx.HLE.HOS.Services.Ssl.SslService
public ResultCode Handshake(string hostName)
{
StartSslOperation();
- _stream = new SslStream(new NetworkStream(((ManagedSocket)Socket).Socket, false), false, null, null);
+ _stream = new SslStream(new NetworkStream(((DefaultSocket)((ManagedSocket)Socket).Socket).BaseSocket, false), false, null, null);
hostName = RetrieveHostName(hostName);
_stream.AuthenticateAsClient(hostName, null, TranslateSslVersion(_sslVersion), false);
EndSslOperation();
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs
index 10561a5a1..e187b2360 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs
@@ -85,8 +85,8 @@ namespace Ryujinx.HLE.Loaders.Processes
}
// TODO: LibHac npdm currently doesn't support version field.
- string version = ProgramId > 0x0100000000007FFF
- ? DisplayVersion
+ string version = ProgramId > 0x0100000000007FFF
+ ? DisplayVersion
: device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?";
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]");
diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
index a7bb3cd7f..5f7f6db69 100644
--- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -29,6 +29,7 @@
+
diff --git a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
index ce52f84d9..8c4854a11 100644
--- a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
+++ b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
@@ -117,8 +117,9 @@ namespace Ryujinx.Headless.SDL2.OpenGL
GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio,
bool enableMouse,
- HideCursorMode hideCursorMode)
- : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
+ HideCursorMode hideCursorMode,
+ bool ignoreControllerApplet)
+ : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
{
_glLogLevel = glLogLevel;
}
diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs
index 2f86f3ebf..8078ca5e4 100644
--- a/src/Ryujinx.Headless.SDL2/Options.cs
+++ b/src/Ryujinx.Headless.SDL2/Options.cs
@@ -225,6 +225,9 @@ namespace Ryujinx.Headless.SDL2
[Option("ignore-missing-services", Required = false, Default = false, HelpText = "Enable ignoring missing services.")]
public bool IgnoreMissingServices { get; set; }
+
+ [Option("ignore-controller-applet", Required = false, Default = false, HelpText = "Enable ignoring the controller applet when your game loses connection to your controller.")]
+ public bool IgnoreControllerApplet { get; set; }
// Values
diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
index b6ccb2ac4..e3bbd1e51 100644
--- a/src/Ryujinx.Headless.SDL2/Program.cs
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -444,8 +444,7 @@ namespace Ryujinx.Headless.SDL2
{
Logger.AddTarget(new AsyncLogTargetWrapper(
new FileLogTarget("file", logFile),
- 1000,
- AsyncLogTargetOverflowAction.Block
+ 1000
));
}
else
@@ -506,8 +505,8 @@ namespace Ryujinx.Headless.SDL2
private static WindowBase CreateWindow(Options options)
{
return options.GraphicsBackend == GraphicsBackend.Vulkan
- ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode)
- : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode);
+ ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet)
+ : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet);
}
private static IRenderer CreateRenderer(Options options, WindowBase window)
@@ -578,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);
}
diff --git a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
index fb73ca335..b88e0fe83 100644
--- a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
+++ b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
@@ -17,8 +17,9 @@ namespace Ryujinx.Headless.SDL2.Vulkan
GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio,
bool enableMouse,
- HideCursorMode hideCursorMode)
- : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
+ HideCursorMode hideCursorMode,
+ bool ignoreControllerApplet)
+ : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
{
_glLogLevel = glLogLevel;
}
diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs
index bd47dfd5d..6d681e100 100644
--- a/src/Ryujinx.Headless.SDL2/WindowBase.cs
+++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs
@@ -86,13 +86,15 @@ namespace Ryujinx.Headless.SDL2
private readonly AspectRatio _aspectRatio;
private readonly bool _enableMouse;
+ private readonly bool _ignoreControllerApplet;
public WindowBase(
InputManager inputManager,
GraphicsDebugLevel glLogLevel,
AspectRatio aspectRatio,
bool enableMouse,
- HideCursorMode hideCursorMode)
+ HideCursorMode hideCursorMode,
+ bool ignoreControllerApplet)
{
MouseDriver = new SDL2MouseDriver(hideCursorMode);
_inputManager = inputManager;
@@ -108,6 +110,7 @@ namespace Ryujinx.Headless.SDL2
_gpuDoneEvent = new ManualResetEvent(false);
_aspectRatio = aspectRatio;
_enableMouse = enableMouse;
+ _ignoreControllerApplet = ignoreControllerApplet;
HostUITheme = new HeadlessHostUiTheme();
SDL2Driver.Instance.Initialize();
@@ -484,6 +487,8 @@ namespace Ryujinx.Headless.SDL2
public bool DisplayMessageDialog(ControllerAppletUIArgs args)
{
+ if (_ignoreControllerApplet) return false;
+
string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
string message = $"Application requests {playerCount} {"player".ToQuantity(args.PlayerCountMin + args.PlayerCountMax, ShowQuantityAs.None)} with:\n\n"
diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs
index 4c1c1a043..7aa0dccaa 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationData.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs
@@ -27,6 +27,8 @@ namespace Ryujinx.UI.App.Common
public ulong Id { get; set; }
public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0";
+ public int PlayerCount { get; set; }
+ public int GameCount { get; set; }
public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }
diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
index 044eccbea..174db51ad 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
@@ -12,6 +12,7 @@ using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
@@ -27,10 +28,12 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
+using System.Threading.Tasks;
using ContentType = LibHac.Ncm.ContentType;
using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
using Path = System.IO.Path;
@@ -41,8 +44,10 @@ namespace Ryujinx.UI.App.Common
{
public class ApplicationLibrary
{
+ public static string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
public Language DesiredLanguage { get; set; }
public event EventHandler ApplicationCountUpdated;
+ public event EventHandler LdnGameDataReceived;
public readonly IObservableCache Applications;
public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates;
@@ -62,6 +67,7 @@ namespace Ryujinx.UI.App.Common
private readonly SourceCache<(DownloadableContentModel Dlc, bool IsEnabled), DownloadableContentModel> _downloadableContents = new(it => it.Dlc);
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+ private static readonly LdnGameDataSerializerContext _ldnDataSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
{
@@ -687,7 +693,7 @@ namespace Ryujinx.UI.App.Common
(Path.GetExtension(file).ToLower() is ".pfs0" && ConfigurationState.Instance.UI.ShownFileTypes.PFS0) ||
(Path.GetExtension(file).ToLower() is ".xci" && ConfigurationState.Instance.UI.ShownFileTypes.XCI) ||
(Path.GetExtension(file).ToLower() is ".nca" && ConfigurationState.Instance.UI.ShownFileTypes.NCA) ||
- (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) ||
+ (Path.GetExtension(file).ToLower() is ".nro" && ConfigurationState.Instance.UI.ShownFileTypes.NRO) ||
(Path.GetExtension(file).ToLower() is ".nso" && ConfigurationState.Instance.UI.ShownFileTypes.NSO)
);
@@ -719,6 +725,7 @@ namespace Ryujinx.UI.App.Common
}
}
+
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
foreach (string applicationPath in applicationPaths)
{
@@ -775,6 +782,46 @@ namespace Ryujinx.UI.App.Common
}
}
+ public async Task RefreshLdn()
+ {
+
+ if (ConfigurationState.Instance.Multiplayer.Mode == MultiplayerMode.LdnRyu)
+ {
+ try
+ {
+ string ldnWebHost = ConfigurationState.Instance.Multiplayer.LdnServer;
+ if (string.IsNullOrEmpty(ldnWebHost))
+ {
+ ldnWebHost = DefaultLanPlayWebHost;
+ }
+ IEnumerable ldnGameDataArray = Array.Empty();
+ using HttpClient httpClient = new HttpClient();
+ string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games");
+ ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData);
+ var evt = new LdnGameDataReceivedEventArgs
+ {
+ LdnData = ldnGameDataArray
+ };
+ LdnGameDataReceived?.Invoke(null, evt);
+ }
+ catch (Exception ex)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.\n{ex.Message}");
+ LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs()
+ {
+ LdnData = Array.Empty()
+ });
+ }
+ }
+ else
+ {
+ LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs()
+ {
+ LdnData = Array.Empty()
+ });
+ }
+ }
+
// Replace the currently stored DLC state for the game with the provided DLC state.
public void SaveDownloadableContentsForGame(ApplicationData application, List<(DownloadableContentModel, bool IsEnabled)> dlcs)
{
diff --git a/src/Ryujinx.UI.Common/App/LdnGameData.cs b/src/Ryujinx.UI.Common/App/LdnGameData.cs
new file mode 100644
index 000000000..6c784c991
--- /dev/null
+++ b/src/Ryujinx.UI.Common/App/LdnGameData.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.UI.App.Common
+{
+ public struct LdnGameData
+ {
+ public string Id { get; set; }
+ public int PlayerCount { get; set; }
+ public int MaxPlayerCount { get; set; }
+ public string GameName { get; set; }
+ public string TitleId { get; set; }
+ public string Mode { get; set; }
+ public string Status { get; set; }
+ public IEnumerable Players { get; set; }
+ }
+}
diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs
new file mode 100644
index 000000000..7c7454411
--- /dev/null
+++ b/src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.UI.App.Common
+{
+ public class LdnGameDataReceivedEventArgs : EventArgs
+ {
+ public IEnumerable LdnData { get; set; }
+ }
+}
diff --git a/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs
new file mode 100644
index 000000000..ce8edcdb6
--- /dev/null
+++ b/src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Ryujinx.UI.App.Common
+{
+ [JsonSerializable(typeof(IEnumerable))]
+ internal partial class LdnGameDataSerializerContext : JsonSerializerContext
+ {
+
+ }
+}
diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs
index 77c6346f2..80ba1b186 100644
--- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs
+++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs
@@ -392,6 +392,21 @@ namespace Ryujinx.UI.Common.Configuration
///
public string MultiplayerLanInterfaceId { get; set; }
+ ///
+ /// Disable P2p Toggle
+ ///
+ public bool MultiplayerDisableP2p { get; set; }
+
+ ///
+ /// Local network passphrase, for private networks.
+ ///
+ public string MultiplayerLdnPassphrase { get; set; }
+
+ ///
+ /// Custom LDN Server
+ ///
+ public string LdnServer { get; set; }
+
///
/// Uses Hypervisor over JIT if available
///
diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs
new file mode 100644
index 000000000..65dd88106
--- /dev/null
+++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs
@@ -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
+ {
+ new StandardKeyboardInputConfig
+ {
+ Version = InputConfig.CurrentVersion,
+ Backend = InputBackendType.WindowKeyboard,
+ Id = "0",
+ PlayerIndex = PlayerIndex.Player1,
+ ControllerType = ControllerType.ProController,
+ LeftJoycon = new LeftJoyconCommonConfig
+ {
+ DpadUp = Key.Up,
+ DpadDown = Key.Down,
+ DpadLeft = Key.Left,
+ DpadRight = Key.Right,
+ ButtonMinus = Key.Minus,
+ ButtonL = Key.E,
+ ButtonZl = Key.Q,
+ ButtonSl = Key.Unbound,
+ ButtonSr = Key.Unbound,
+ },
+ LeftJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickUp = Key.W,
+ StickDown = Key.S,
+ StickLeft = Key.A,
+ StickRight = Key.D,
+ StickButton = Key.F,
+ },
+ RightJoycon = new RightJoyconCommonConfig
+ {
+ ButtonA = Key.Z,
+ ButtonB = Key.X,
+ ButtonX = Key.C,
+ ButtonY = Key.V,
+ ButtonPlus = Key.Plus,
+ ButtonR = Key.U,
+ ButtonZr = Key.O,
+ ButtonSl = Key.Unbound,
+ ButtonSr = Key.Unbound,
+ },
+ RightJoyconStick = new JoyconConfigKeyboardStick
+ {
+ StickUp = Key.I,
+ StickDown = Key.K,
+ StickLeft = Key.J,
+ StickRight = Key.L,
+ StickButton = Key.H,
+ },
+ },
+ };
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 25)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 25.");
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 26)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 26.");
+
+ configurationFileFormat.MemoryManagerMode = MemoryManagerMode.HostMappedUnsafe;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 27)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 27.");
+
+ configurationFileFormat.EnableMouse = false;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 28)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 28.");
+
+ configurationFileFormat.Hotkeys = new KeyboardHotkeys
+ {
+ ToggleVsync = Key.F1,
+ Screenshot = Key.F8,
+ };
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 29)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 29.");
+
+ configurationFileFormat.Hotkeys = new KeyboardHotkeys
+ {
+ ToggleVsync = Key.F1,
+ Screenshot = Key.F8,
+ ShowUI = Key.F4,
+ };
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 30)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30.");
+
+ foreach (InputConfig config in configurationFileFormat.InputConfig)
+ {
+ if (config is StandardControllerInputConfig controllerConfig)
+ {
+ controllerConfig.Rumble = new RumbleConfigController
+ {
+ EnableRumble = false,
+ StrongRumble = 1f,
+ WeakRumble = 1f,
+ };
+ }
+ }
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 31)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 31.");
+
+ configurationFileFormat.BackendThreading = BackendThreading.Auto;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 32)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 32.");
+
+ configurationFileFormat.Hotkeys = new KeyboardHotkeys
+ {
+ ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
+ Screenshot = configurationFileFormat.Hotkeys.Screenshot,
+ ShowUI = configurationFileFormat.Hotkeys.ShowUI,
+ Pause = Key.F5,
+ };
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 33)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 33.");
+
+ configurationFileFormat.Hotkeys = new KeyboardHotkeys
+ {
+ ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
+ Screenshot = configurationFileFormat.Hotkeys.Screenshot,
+ ShowUI = configurationFileFormat.Hotkeys.ShowUI,
+ Pause = configurationFileFormat.Hotkeys.Pause,
+ ToggleMute = Key.F2,
+ };
+
+ configurationFileFormat.AudioVolume = 1;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 34)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 34.");
+
+ configurationFileFormat.EnableInternetAccess = false;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 35)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 35.");
+
+ foreach (InputConfig config in configurationFileFormat.InputConfig)
+ {
+ if (config is StandardControllerInputConfig controllerConfig)
+ {
+ controllerConfig.RangeLeft = 1.0f;
+ controllerConfig.RangeRight = 1.0f;
+ }
+ }
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 36)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 36.");
+
+ configurationFileFormat.LoggingEnableTrace = false;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 37)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 37.");
+
+ configurationFileFormat.ShowConsole = true;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 38)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 38.");
+
+ configurationFileFormat.BaseStyle = "Dark";
+ configurationFileFormat.GameListViewMode = 0;
+ configurationFileFormat.ShowNames = true;
+ configurationFileFormat.GridSize = 2;
+ configurationFileFormat.LanguageCode = "en_US";
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 39)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 39.");
+
+ configurationFileFormat.Hotkeys = new KeyboardHotkeys
+ {
+ ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
+ Screenshot = configurationFileFormat.Hotkeys.Screenshot,
+ ShowUI = configurationFileFormat.Hotkeys.ShowUI,
+ Pause = configurationFileFormat.Hotkeys.Pause,
+ ToggleMute = configurationFileFormat.Hotkeys.ToggleMute,
+ ResScaleUp = Key.Unbound,
+ ResScaleDown = Key.Unbound,
+ };
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 40)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 40.");
+
+ configurationFileFormat.GraphicsBackend = GraphicsBackend.OpenGl;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 41)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 41.");
+
+ configurationFileFormat.Hotkeys = new KeyboardHotkeys
+ {
+ ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
+ Screenshot = configurationFileFormat.Hotkeys.Screenshot,
+ ShowUI = configurationFileFormat.Hotkeys.ShowUI,
+ Pause = configurationFileFormat.Hotkeys.Pause,
+ ToggleMute = configurationFileFormat.Hotkeys.ToggleMute,
+ ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp,
+ ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown,
+ VolumeUp = Key.Unbound,
+ VolumeDown = Key.Unbound,
+ };
+ }
+
+ if (configurationFileFormat.Version < 42)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 42.");
+
+ configurationFileFormat.EnableMacroHLE = true;
+ }
+
+ if (configurationFileFormat.Version < 43)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 43.");
+
+ configurationFileFormat.UseHypervisor = true;
+ }
+
+ if (configurationFileFormat.Version < 44)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 44.");
+
+ configurationFileFormat.AntiAliasing = AntiAliasing.None;
+ configurationFileFormat.ScalingFilter = ScalingFilter.Bilinear;
+ configurationFileFormat.ScalingFilterLevel = 80;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 45)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 45.");
+
+ configurationFileFormat.ShownFileTypes = new ShownFileTypes
+ {
+ NSP = true,
+ PFS0 = true,
+ XCI = true,
+ NCA = true,
+ NRO = true,
+ NSO = true,
+ };
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 46)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 46.");
+
+ configurationFileFormat.MultiplayerLanInterfaceId = "0";
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 47)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 47.");
+
+ configurationFileFormat.WindowStartup = new WindowStartup
+ {
+ WindowPositionX = 0,
+ WindowPositionY = 0,
+ WindowSizeHeight = 760,
+ WindowSizeWidth = 1280,
+ WindowMaximized = false,
+ };
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 48)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 48.");
+
+ configurationFileFormat.EnableColorSpacePassthrough = false;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 49)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 49.");
+
+ if (OperatingSystem.IsMacOS())
+ {
+ AppDataManager.FixMacOSConfigurationFolders();
+ }
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 50)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 50.");
+
+ configurationFileFormat.EnableHardwareAcceleration = true;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 51)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51.");
+
+ configurationFileFormat.RememberWindowState = true;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 52)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 52.");
+
+ configurationFileFormat.AutoloadDirs = [];
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 53)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 53.");
+
+ configurationFileFormat.EnableLowPowerPtc = false;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 54)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 54.");
+
+ configurationFileFormat.DramSize = MemoryConfiguration.MemoryConfiguration4GiB;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 55)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 55.");
+
+ configurationFileFormat.IgnoreApplet = false;
+
+ configurationFileUpdated = true;
+ }
+
+ if (configurationFileFormat.Version < 56)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 56.");
+
+ configurationFileFormat.ShowTitleBar = !OperatingSystem.IsWindows();
+
+ configurationFileUpdated = true;
+ }
+
+ Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
+ Graphics.ResScale.Value = configurationFileFormat.ResScale;
+ Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
+ Graphics.MaxAnisotropy.Value = configurationFileFormat.MaxAnisotropy;
+ Graphics.AspectRatio.Value = configurationFileFormat.AspectRatio;
+ Graphics.ShadersDumpPath.Value = configurationFileFormat.GraphicsShadersDumpPath;
+ Graphics.BackendThreading.Value = configurationFileFormat.BackendThreading;
+ Graphics.GraphicsBackend.Value = configurationFileFormat.GraphicsBackend;
+ Graphics.PreferredGpu.Value = configurationFileFormat.PreferredGpu;
+ Graphics.AntiAliasing.Value = configurationFileFormat.AntiAliasing;
+ Graphics.ScalingFilter.Value = configurationFileFormat.ScalingFilter;
+ Graphics.ScalingFilterLevel.Value = configurationFileFormat.ScalingFilterLevel;
+ Logger.EnableDebug.Value = configurationFileFormat.LoggingEnableDebug;
+ Logger.EnableStub.Value = configurationFileFormat.LoggingEnableStub;
+ Logger.EnableInfo.Value = configurationFileFormat.LoggingEnableInfo;
+ Logger.EnableWarn.Value = configurationFileFormat.LoggingEnableWarn;
+ Logger.EnableError.Value = configurationFileFormat.LoggingEnableError;
+ Logger.EnableTrace.Value = configurationFileFormat.LoggingEnableTrace;
+ Logger.EnableGuest.Value = configurationFileFormat.LoggingEnableGuest;
+ Logger.EnableFsAccessLog.Value = configurationFileFormat.LoggingEnableFsAccessLog;
+ Logger.FilteredClasses.Value = configurationFileFormat.LoggingFilteredClasses;
+ Logger.GraphicsDebugLevel.Value = configurationFileFormat.LoggingGraphicsDebugLevel;
+ System.Language.Value = configurationFileFormat.SystemLanguage;
+ System.Region.Value = configurationFileFormat.SystemRegion;
+ System.TimeZone.Value = configurationFileFormat.SystemTimeZone;
+ System.SystemTimeOffset.Value = configurationFileFormat.SystemTimeOffset;
+ System.EnableDockedMode.Value = configurationFileFormat.DockedMode;
+ EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration;
+ CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart;
+ ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit;
+ IgnoreApplet.Value = configurationFileFormat.IgnoreApplet;
+ RememberWindowState.Value = configurationFileFormat.RememberWindowState;
+ ShowTitleBar.Value = configurationFileFormat.ShowTitleBar;
+ EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration;
+ HideCursor.Value = configurationFileFormat.HideCursor;
+ Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync;
+ Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache;
+ Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression;
+ Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE;
+ Graphics.EnableColorSpacePassthrough.Value = configurationFileFormat.EnableColorSpacePassthrough;
+ System.EnablePtc.Value = configurationFileFormat.EnablePtc;
+ System.EnableLowPowerPtc.Value = configurationFileFormat.EnableLowPowerPtc;
+ System.EnableInternetAccess.Value = configurationFileFormat.EnableInternetAccess;
+ System.EnableFsIntegrityChecks.Value = configurationFileFormat.EnableFsIntegrityChecks;
+ System.FsGlobalAccessLogMode.Value = configurationFileFormat.FsGlobalAccessLogMode;
+ System.AudioBackend.Value = configurationFileFormat.AudioBackend;
+ System.AudioVolume.Value = configurationFileFormat.AudioVolume;
+ System.MemoryManagerMode.Value = configurationFileFormat.MemoryManagerMode;
+ System.DramSize.Value = configurationFileFormat.DramSize;
+ System.IgnoreMissingServices.Value = configurationFileFormat.IgnoreMissingServices;
+ System.UseHypervisor.Value = configurationFileFormat.UseHypervisor;
+ UI.GuiColumns.FavColumn.Value = configurationFileFormat.GuiColumns.FavColumn;
+ UI.GuiColumns.IconColumn.Value = configurationFileFormat.GuiColumns.IconColumn;
+ UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn;
+ UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn;
+ UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn;
+ UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn;
+ UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn;
+ UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn;
+ UI.GuiColumns.FileSizeColumn.Value = configurationFileFormat.GuiColumns.FileSizeColumn;
+ UI.GuiColumns.PathColumn.Value = configurationFileFormat.GuiColumns.PathColumn;
+ UI.ColumnSort.SortColumnId.Value = configurationFileFormat.ColumnSort.SortColumnId;
+ UI.ColumnSort.SortAscending.Value = configurationFileFormat.ColumnSort.SortAscending;
+ UI.GameDirs.Value = configurationFileFormat.GameDirs;
+ UI.AutoloadDirs.Value = configurationFileFormat.AutoloadDirs ?? [];
+ UI.ShownFileTypes.NSP.Value = configurationFileFormat.ShownFileTypes.NSP;
+ UI.ShownFileTypes.PFS0.Value = configurationFileFormat.ShownFileTypes.PFS0;
+ UI.ShownFileTypes.XCI.Value = configurationFileFormat.ShownFileTypes.XCI;
+ UI.ShownFileTypes.NCA.Value = configurationFileFormat.ShownFileTypes.NCA;
+ UI.ShownFileTypes.NRO.Value = configurationFileFormat.ShownFileTypes.NRO;
+ UI.ShownFileTypes.NSO.Value = configurationFileFormat.ShownFileTypes.NSO;
+ UI.LanguageCode.Value = configurationFileFormat.LanguageCode;
+ UI.BaseStyle.Value = configurationFileFormat.BaseStyle;
+ UI.GameListViewMode.Value = configurationFileFormat.GameListViewMode;
+ UI.ShowNames.Value = configurationFileFormat.ShowNames;
+ UI.IsAscendingOrder.Value = configurationFileFormat.IsAscendingOrder;
+ UI.GridSize.Value = configurationFileFormat.GridSize;
+ UI.ApplicationSort.Value = configurationFileFormat.ApplicationSort;
+ UI.StartFullscreen.Value = configurationFileFormat.StartFullscreen;
+ UI.ShowConsole.Value = configurationFileFormat.ShowConsole;
+ UI.WindowStartup.WindowSizeWidth.Value = configurationFileFormat.WindowStartup.WindowSizeWidth;
+ UI.WindowStartup.WindowSizeHeight.Value = configurationFileFormat.WindowStartup.WindowSizeHeight;
+ UI.WindowStartup.WindowPositionX.Value = configurationFileFormat.WindowStartup.WindowPositionX;
+ UI.WindowStartup.WindowPositionY.Value = configurationFileFormat.WindowStartup.WindowPositionY;
+ UI.WindowStartup.WindowMaximized.Value = configurationFileFormat.WindowStartup.WindowMaximized;
+ Hid.EnableKeyboard.Value = configurationFileFormat.EnableKeyboard;
+ Hid.EnableMouse.Value = configurationFileFormat.EnableMouse;
+ Hid.Hotkeys.Value = configurationFileFormat.Hotkeys;
+ Hid.InputConfig.Value = configurationFileFormat.InputConfig ?? [];
+
+ Multiplayer.LanInterfaceId.Value = configurationFileFormat.MultiplayerLanInterfaceId;
+ Multiplayer.Mode.Value = configurationFileFormat.MultiplayerMode;
+ Multiplayer.DisableP2p.Value = configurationFileFormat.MultiplayerDisableP2p;
+ Multiplayer.LdnPassphrase.Value = configurationFileFormat.MultiplayerLdnPassphrase;
+ Multiplayer.LdnServer.Value = configurationFileFormat.LdnServer;
+
+ if (configurationFileUpdated)
+ {
+ ToFileFormat().SaveConfig(configurationFilePath);
+
+ Ryujinx.Common.Logging.Logger.Notice.Print(LogClass.Application, $"Configuration file updated to version {ConfigurationFileFormat.CurrentVersion}");
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs
new file mode 100644
index 000000000..9be8f4df7
--- /dev/null
+++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs
@@ -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
+ {
+ ///
+ /// UI configuration section
+ ///
+ public class UISection
+ {
+ public class Columns
+ {
+ public ReactiveObject FavColumn { get; private set; }
+ public ReactiveObject IconColumn { get; private set; }
+ public ReactiveObject AppColumn { get; private set; }
+ public ReactiveObject DevColumn { get; private set; }
+ public ReactiveObject VersionColumn { get; private set; }
+ public ReactiveObject LdnInfoColumn { get; private set; }
+ public ReactiveObject TimePlayedColumn { get; private set; }
+ public ReactiveObject LastPlayedColumn { get; private set; }
+ public ReactiveObject FileExtColumn { get; private set; }
+ public ReactiveObject FileSizeColumn { get; private set; }
+ public ReactiveObject PathColumn { get; private set; }
+
+ public Columns()
+ {
+ FavColumn = new ReactiveObject();
+ IconColumn = new ReactiveObject();
+ AppColumn = new ReactiveObject();
+ DevColumn = new ReactiveObject();
+ VersionColumn = new ReactiveObject();
+ LdnInfoColumn = new ReactiveObject();
+ TimePlayedColumn = new ReactiveObject();
+ LastPlayedColumn = new ReactiveObject();
+ FileExtColumn = new ReactiveObject();
+ FileSizeColumn = new ReactiveObject();
+ PathColumn = new ReactiveObject();
+ }
+ }
+
+ public class ColumnSortSettings
+ {
+ public ReactiveObject SortColumnId { get; private set; }
+ public ReactiveObject SortAscending { get; private set; }
+
+ public ColumnSortSettings()
+ {
+ SortColumnId = new ReactiveObject();
+ SortAscending = new ReactiveObject();
+ }
+ }
+
+ ///
+ /// Used to toggle which file types are shown in the UI
+ ///
+ public class ShownFileTypeSettings
+ {
+ public ReactiveObject NSP { get; private set; }
+ public ReactiveObject PFS0 { get; private set; }
+ public ReactiveObject XCI { get; private set; }
+ public ReactiveObject NCA { get; private set; }
+ public ReactiveObject NRO { get; private set; }
+ public ReactiveObject NSO { get; private set; }
+
+ public ShownFileTypeSettings()
+ {
+ NSP = new ReactiveObject();
+ PFS0 = new ReactiveObject();
+ XCI = new ReactiveObject();
+ NCA = new ReactiveObject();
+ NRO = new ReactiveObject();
+ NSO = new ReactiveObject();
+ }
+ }
+
+ //
+ /// Determines main window start-up position, size and state
+ ///
+ public class WindowStartupSettings
+ {
+ public ReactiveObject WindowSizeWidth { get; private set; }
+ public ReactiveObject WindowSizeHeight { get; private set; }
+ public ReactiveObject WindowPositionX { get; private set; }
+ public ReactiveObject WindowPositionY { get; private set; }
+ public ReactiveObject WindowMaximized { get; private set; }
+
+ public WindowStartupSettings()
+ {
+ WindowSizeWidth = new ReactiveObject();
+ WindowSizeHeight = new ReactiveObject();
+ WindowPositionX = new ReactiveObject();
+ WindowPositionY = new ReactiveObject();
+ WindowMaximized = new ReactiveObject();
+ }
+ }
+
+ ///