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/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/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/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/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/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
index 78cdb7718..e3bbd1e51 100644
--- a/src/Ryujinx.Headless.SDL2/Program.cs
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -577,7 +577,10 @@ namespace Ryujinx.Headless.SDL2
options.AudioVolume,
options.UseHypervisor ?? true,
options.MultiplayerLanInterfaceId,
- Common.Configuration.Multiplayer.MultiplayerMode.Disabled);
+ Common.Configuration.Multiplayer.MultiplayerMode.Disabled,
+ false,
+ "",
+ "");
return new Switch(configuration);
}
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
index 18326a3a4..65dd88106 100644
--- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs
+++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Migration.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
@@ -703,6 +703,9 @@ namespace Ryujinx.UI.Common.Configuration
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)
{
diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs
index 2b43b2032..9be8f4df7 100644
--- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs
+++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.Model.cs
@@ -13,7 +13,7 @@ namespace Ryujinx.UI.Common.Configuration
{
public partial class ConfigurationState
{
- ///
+ ///
/// UI configuration section
///
public class UISection
@@ -25,6 +25,7 @@ namespace Ryujinx.UI.Common.Configuration
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; }
@@ -38,6 +39,7 @@ namespace Ryujinx.UI.Common.Configuration
AppColumn = new ReactiveObject();
DevColumn = new ReactiveObject();
VersionColumn = new ReactiveObject();
+ LdnInfoColumn = new ReactiveObject();
TimePlayedColumn = new ReactiveObject();
LastPlayedColumn = new ReactiveObject();
FileExtColumn = new ReactiveObject();
@@ -572,11 +574,32 @@ namespace Ryujinx.UI.Common.Configuration
///
public ReactiveObject Mode { get; private set; }
+ ///
+ /// Disable P2P
+ ///
+ public ReactiveObject DisableP2p { get; private set; }
+
+ ///
+ /// LDN PassPhrase
+ ///
+ public ReactiveObject LdnPassphrase { get; private set; }
+
+ ///
+ /// LDN Server
+ ///
+ public ReactiveObject LdnServer { get; private set; }
+
public MultiplayerSection()
{
LanInterfaceId = new ReactiveObject();
Mode = new ReactiveObject();
Mode.LogChangesToValue(nameof(MultiplayerMode));
+ DisableP2p = new ReactiveObject();
+ DisableP2p.LogChangesToValue(nameof(DisableP2p));
+ LdnPassphrase = new ReactiveObject();
+ LdnPassphrase.LogChangesToValue(nameof(LdnPassphrase));
+ LdnServer = new ReactiveObject();
+ LdnServer.LogChangesToValue(nameof(LdnServer));
}
}
diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
index 6fd5cfb93..b3012568e 100644
--- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
@@ -21,11 +21,11 @@ namespace Ryujinx.UI.Common.Configuration
if (Instance != null)
{
throw new InvalidOperationException("Configuration is already initialized");
- }
+ }
Instance = new ConfigurationState();
- }
-
+ }
+
public ConfigurationFileFormat ToFileFormat()
{
ConfigurationFileFormat configurationFile = new()
@@ -87,6 +87,7 @@ namespace Ryujinx.UI.Common.Configuration
AppColumn = UI.GuiColumns.AppColumn,
DevColumn = UI.GuiColumns.DevColumn,
VersionColumn = UI.GuiColumns.VersionColumn,
+ LdnInfoColumn = UI.GuiColumns.LdnInfoColumn,
TimePlayedColumn = UI.GuiColumns.TimePlayedColumn,
LastPlayedColumn = UI.GuiColumns.LastPlayedColumn,
FileExtColumn = UI.GuiColumns.FileExtColumn,
@@ -136,6 +137,9 @@ namespace Ryujinx.UI.Common.Configuration
PreferredGpu = Graphics.PreferredGpu,
MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId,
MultiplayerMode = Multiplayer.Mode,
+ MultiplayerDisableP2p = Multiplayer.DisableP2p,
+ MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase,
+ LdnServer = Multiplayer.LdnServer,
};
return configurationFile;
@@ -195,6 +199,9 @@ namespace Ryujinx.UI.Common.Configuration
System.UseHypervisor.Value = true;
Multiplayer.LanInterfaceId.Value = "0";
Multiplayer.Mode.Value = MultiplayerMode.Disabled;
+ Multiplayer.DisableP2p.Value = false;
+ Multiplayer.LdnPassphrase.Value = "";
+ Multiplayer.LdnServer.Value = "";
UI.GuiColumns.FavColumn.Value = true;
UI.GuiColumns.IconColumn.Value = true;
UI.GuiColumns.AppColumn.Value = true;
@@ -307,5 +314,5 @@ namespace Ryujinx.UI.Common.Configuration
return GraphicsBackend.OpenGl;
}
- }
-}
+ }
+ }
diff --git a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs
index c778ef1f1..c486492e0 100644
--- a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs
+++ b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs
@@ -7,6 +7,7 @@ namespace Ryujinx.UI.Common.Configuration.UI
public bool AppColumn { get; set; }
public bool DevColumn { get; set; }
public bool VersionColumn { get; set; }
+ public bool LdnInfoColumn { get; set; }
public bool TimePlayedColumn { get; set; }
public bool LastPlayedColumn { get; set; }
public bool FileExtColumn { get; set; }
diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs
index dc4f4ff36..7246be4b9 100644
--- a/src/Ryujinx/AppHost.cs
+++ b/src/Ryujinx/AppHost.cs
@@ -207,6 +207,9 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
+ ConfigurationState.Instance.Multiplayer.LdnPassphrase.Event += UpdateLdnPassphraseState;
+ ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState;
+ ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState;
_gpuCancellationTokenSource = new CancellationTokenSource();
_gpuDoneEvent = new ManualResetEvent(false);
@@ -491,6 +494,21 @@ namespace Ryujinx.Ava
Device.Configuration.MultiplayerMode = e.NewValue;
}
+ private void UpdateLdnPassphraseState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.MultiplayerLdnPassphrase = e.NewValue;
+ }
+
+ private void UpdateLdnServerState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.MultiplayerLdnServer = e.NewValue;
+ }
+
+ private void UpdateDisableP2pState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.MultiplayerDisableP2p = e.NewValue;
+ }
+
public void ToggleVSync()
{
Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
@@ -863,10 +881,11 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Graphics.AspectRatio,
ConfigurationState.Instance.System.AudioVolume,
ConfigurationState.Instance.System.UseHypervisor,
- ConfigurationState.Instance.Multiplayer.LanInterfaceId,
- ConfigurationState.Instance.Multiplayer.Mode
- )
- );
+ ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value,
+ ConfigurationState.Instance.Multiplayer.Mode,
+ ConfigurationState.Instance.Multiplayer.DisableP2p,
+ ConfigurationState.Instance.Multiplayer.LdnPassphrase,
+ ConfigurationState.Instance.Multiplayer.LdnServer));
}
private static IHardwareDeviceDriver InitializeAudio()
@@ -1050,7 +1069,7 @@ namespace Ryujinx.Ava
string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld];
UpdateShaderCount();
-
+
if (GraphicsConfig.ResScale != 1)
{
dockedMode += $" ({GraphicsConfig.ResScale}x)";
diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json
index 88eb43ce4..fdd2d4df2 100644
--- a/src/Ryujinx/Assets/Locales/en_US.json
+++ b/src/Ryujinx/Assets/Locales/en_US.json
@@ -848,5 +848,17 @@
"MultiplayerMode": "Mode:",
"MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.",
"MultiplayerModeDisabled": "Disabled",
- "MultiplayerModeLdnMitm": "ldn_mitm"
+ "MultiplayerModeLdnMitm": "ldn_mitm",
+ "MultiplayerModeLdnRyu": "RyuLDN",
+ "MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)",
+ "MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.",
+ "LdnPassphrase": "Network Passphrase:",
+ "LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.",
+ "LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.",
+ "LdnPassphraseInputPublic": "(public)",
+ "GenLdnPass": "Generate Random",
+ "GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.",
+ "ClearLdnPass": "Clear",
+ "ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.",
+ "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\""
}
diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs
index 3247a55f8..b57caa468 100644
--- a/src/Ryujinx/Common/LocaleManager.cs
+++ b/src/Ryujinx/Common/LocaleManager.cs
@@ -99,7 +99,7 @@ namespace Ryujinx.Ava.Common.Locale
_ => false
};
- public static string FormatDynamicValue(LocaleKeys key, params object[] values)
+ public static string FormatDynamicValue(LocaleKeys key, params object[] values)
=> Instance.UpdateAndGetDynamicValue(key, values);
public string UpdateAndGetDynamicValue(LocaleKeys key, params object[] values)
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 76512a34a..f78b8e972 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -103,7 +103,7 @@ namespace Ryujinx.Ava
Console.Title = $"{App.FullAppName} Console {Version}";
// Hook unhandled exception and process exit events.
- AppDomain.CurrentDomain.UnhandledException += (sender, e)
+ AppDomain.CurrentDomain.UnhandledException += (sender, e)
=> ProcessUnhandledException(sender, e.ExceptionObject as Exception, e.IsTerminating);
AppDomain.CurrentDomain.ProcessExit += (_, _) => Exit();
@@ -229,11 +229,9 @@ namespace Ryujinx.Ava
var enabledLogLevels = Logger.GetEnabledLevels().ToArray();
- Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {
- (enabledLogLevels.Length is 0
+ Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogLevels.Length is 0
? ""
- : enabledLogLevels.JoinToString(", "))
- }");
+ : enabledLogLevels.JoinToString(", "))}");
Logger.Notice.Print(LogClass.Application,
AppDataManager.Mode == AppDataManager.LaunchMode.Custom
@@ -245,13 +243,13 @@ namespace Ryujinx.Ava
{
Logger.Log log = Logger.Error ?? Logger.Notice;
string message = $"Unhandled exception caught: {ex}";
-
+
// ReSharper disable once ConstantConditionalAccessQualifier
- if (sender?.GetType()?.AsPrettyString() is {} senderName)
+ if (sender?.GetType()?.AsPrettyString() is { } senderName)
log.Print(LogClass.Application, message, senderName);
else
log.PrintMsg(LogClass.Application, message);
-
+
if (isTerminating)
Exit();
}
diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
index 1dbf37255..2ebba7ac0 100644
--- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
+++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
@@ -31,7 +31,7 @@ namespace Ryujinx.Ava.UI.Applet
public bool DisplayMessageDialog(ControllerAppletUIArgs args)
{
ManualResetEvent dialogCloseEvent = new(false);
-
+
bool okPressed = false;
if (ConfigurationState.Instance.IgnoreApplet)
diff --git a/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs b/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs
index 5ec7737ed..0cd3f18e5 100644
--- a/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs
+++ b/src/Ryujinx/UI/Applet/AvaloniaDynamicTextInputHandler.cs
@@ -24,7 +24,7 @@ namespace Ryujinx.Ava.UI.Applet
public AvaloniaDynamicTextInputHandler(MainWindow parent)
{
_parent = parent;
-
+
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver)
{
avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed;
@@ -121,7 +121,7 @@ namespace Ryujinx.Ava.UI.Applet
avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
}
-
+
_textChangedSubscription?.Dispose();
_selectionStartChangedSubscription?.Dispose();
_selectionEndtextChangedSubscription?.Dispose();
diff --git a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs
index 3a8350893..ee0e884d2 100644
--- a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs
+++ b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs
@@ -37,8 +37,8 @@ namespace Ryujinx.Ava.UI.Applet
public ControllerAppletDialog(MainWindow mainWindow, ControllerAppletUIArgs args)
{
- PlayerCount = args.PlayerCountMin == args.PlayerCountMax
- ? args.PlayerCountMin.ToString()
+ PlayerCount = args.PlayerCountMin == args.PlayerCountMax
+ ? args.PlayerCountMin.ToString()
: $"{args.PlayerCountMin} - {args.PlayerCountMax}";
SupportsProController = (args.SupportedStyles & ControllerType.ProController) != 0;
diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml
index ea748a3bf..0daa77ac4 100644
--- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml
@@ -7,6 +7,7 @@
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:converters="clr-namespace:Avalonia.Data.Converters;assembly=Avalonia.Base"
d:DesignHeight="450"
d:DesignWidth="800"
Focusable="True"
@@ -110,6 +111,11 @@
Text="{Binding FileExtension}"
TextAlignment="Start"
TextWrapping="Wrap" />
+
UserFirmwareAvatarSelectorViewModel.PreloadAvatars(contentManager, virtualFileSystem));
-
+
InitializeComponent();
}
@@ -60,13 +60,13 @@ namespace Ryujinx.Ava.UI.Controls
LoadProfiles();
}
- public void Navigate(Type sourcePageType, object parameter)
+ public void Navigate(Type sourcePageType, object parameter)
=> ContentFrame.Navigate(sourcePageType, parameter);
public static async Task Show(
- AccountManager ownerAccountManager,
+ AccountManager ownerAccountManager,
ContentManager ownerContentManager,
- VirtualFileSystem ownerVirtualFileSystem,
+ VirtualFileSystem ownerVirtualFileSystem,
HorizonClient ownerHorizonClient)
{
var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
@@ -158,9 +158,9 @@ namespace Ryujinx.Ava.UI.Controls
_ = Dispatcher.UIThread.InvokeAsync(async ()
=> await ContentDialogHelper.CreateErrorDialog(
LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]));
-
+
return;
- }
+ }
AccountManager.OpenUser(profile.UserId);
}
diff --git a/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs
index 94c3ab35d..6196421c8 100644
--- a/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs
+++ b/src/Ryujinx/UI/Helpers/GlyphValueConverter.cs
@@ -22,9 +22,9 @@ namespace Ryujinx.Ava.UI.Helpers
_key = key;
}
- public string this[string key] =>
+ public string this[string key] =>
_glyphs.TryGetValue(Enum.Parse(key), out var val)
- ? val
+ ? val
: string.Empty;
public override object ProvideValue(IServiceProvider serviceProvider) => this[_key];
diff --git a/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs b/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs
new file mode 100644
index 000000000..8bd8b5f0d
--- /dev/null
+++ b/src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs
@@ -0,0 +1,44 @@
+using Avalonia.Data.Converters;
+using Avalonia.Markup.Xaml;
+using Ryujinx.Ava.Common.Locale;
+using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common.Helper;
+using System;
+using System.Globalization;
+
+namespace Ryujinx.Ava.UI.Helpers
+{
+ internal class MultiplayerInfoConverter : MarkupExtension, IValueConverter
+ {
+ private static readonly MultiplayerInfoConverter _instance = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is ApplicationData applicationData)
+ {
+ if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0)
+ {
+ return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}";
+ }
+ else
+ {
+ return "";
+ }
+ }
+ else
+ {
+ return "";
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ return _instance;
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs b/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs
index 5edc6482e..0e5525c7b 100644
--- a/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs
+++ b/src/Ryujinx/UI/Helpers/TimeZoneConverter.cs
@@ -9,12 +9,12 @@ namespace Ryujinx.Ava.UI.Helpers
{
public static TimeZoneConverter Instance = new();
- public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
- => value is TimeZone timeZone
- ? $"{timeZone.UtcDifference} {timeZone.Location} {timeZone.Abbreviation}"
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ => value is TimeZone timeZone
+ ? $"{timeZone.UtcDifference} {timeZone.Location} {timeZone.Abbreviation}"
: null;
- public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}
diff --git a/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs
index f12cf0aa6..40f783c44 100644
--- a/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs
+++ b/src/Ryujinx/UI/Models/StatusUpdatedEventArgs.cs
@@ -10,7 +10,7 @@ namespace Ryujinx.Ava.UI.Models
public string DockedMode { get; }
public string FifoStatus { get; }
public string GameStatus { get; }
-
+
public uint ShaderCount { get; }
public StatusUpdatedEventArgs(bool vSyncEnabled, string volumeStatus, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, uint shaderCount)
diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
index 437f9861d..53263847b 100644
--- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
@@ -117,6 +117,8 @@ namespace Ryujinx.Ava.UI.ViewModels
public ApplicationData ListSelectedApplication;
public ApplicationData GridSelectedApplication;
+ public IEnumerable LastLdnGameData;
+
public static readonly Bitmap IconBitmap =
new(Assembly.GetAssembly(typeof(ConfigurationState))!.GetManifestResourceStream("Ryujinx.UI.Common.Resources.Logo_Ryujinx.png")!);
@@ -173,7 +175,7 @@ namespace Ryujinx.Ava.UI.ViewModels
SwitchToGameControl = switchToGameControl;
SetMainContent = setMainContent;
TopLevel = topLevel;
-
+
#if DEBUG
topLevel.AttachDevTools(new KeyGesture(Avalonia.Input.Key.F12, KeyModifiers.Control));
#endif
@@ -268,7 +270,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool ShowFirmwareStatus => !ShowLoadProgress;
- public bool ShowRightmostSeparator
+ public bool ShowRightmostSeparator
{
get => _showRightmostSeparator;
set
@@ -553,7 +555,7 @@ namespace Ryujinx.Ava.UI.ViewModels
OnPropertyChanged();
}
}
-
+
public string ShaderCountText
{
get => _shaderCountText;
@@ -1021,7 +1023,7 @@ namespace Ryujinx.Ava.UI.ViewModels
? SortExpressionComparer.Ascending(selector)
: SortExpressionComparer.Descending(selector);
- private IComparer GetComparer()
+ private IComparer GetComparer()
=> SortMode switch
{
#pragma warning disable IDE0055 // Disable formatting
@@ -1251,7 +1253,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void InitializeGame()
{
RendererHostControl.WindowCreated += RendererHost_Created;
-
+
AppHost.StatusUpdatedEvent += Update_StatusBar;
AppHost.AppExit += AppHost_AppExit;
@@ -1300,9 +1302,9 @@ namespace Ryujinx.Ava.UI.ViewModels
GameStatusText = args.GameStatus;
VolumeStatusText = args.VolumeStatus;
FifoStatusText = args.FifoStatus;
-
- ShaderCountText = (ShowRightmostSeparator = args.ShaderCount > 0)
- ? $"{LocaleManager.Instance[LocaleKeys.CompilingShaders]}: {args.ShaderCount}"
+
+ ShaderCountText = (ShowRightmostSeparator = args.ShaderCount > 0)
+ ? $"{LocaleManager.Instance[LocaleKeys.CompilingShaders]}: {args.ShaderCount}"
: string.Empty;
ShowStatusSeparator = true;
@@ -1707,7 +1709,7 @@ namespace Ryujinx.Ava.UI.ViewModels
RendererHostControl.Focus();
});
- public static void UpdateGameMetadata(string titleId)
+ public static void UpdateGameMetadata(string titleId)
=> ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame());
public void RefreshFirmwareStatus()
diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
index f069896f8..2da252d00 100644
--- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
@@ -25,12 +25,13 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using TimeZone = Ryujinx.Ava.UI.Models.TimeZone;
namespace Ryujinx.Ava.UI.ViewModels
{
- public class SettingsViewModel : BaseModel
+ public partial class SettingsViewModel : BaseModel
{
private readonly VirtualFileSystem _virtualFileSystem;
private readonly ContentManager _contentManager;
@@ -56,6 +57,8 @@ namespace Ryujinx.Ava.UI.ViewModels
public event Action SaveSettingsEvent;
private int _networkInterfaceIndex;
private int _multiplayerModeIndex;
+ private string _ldnPassphrase;
+ private string _LdnServer;
public int ResolutionScale
{
@@ -180,10 +183,24 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsVulkanSelected => GraphicsBackendIndex == 0;
public bool UseHypervisor { get; set; }
+ public bool DisableP2P { get; set; }
public string TimeZone { get; set; }
public string ShaderDumpPath { get; set; }
+ public string LdnPassphrase
+ {
+ get => _ldnPassphrase;
+ set
+ {
+ _ldnPassphrase = value;
+ IsInvalidLdnPassphraseVisible = !ValidateLdnPassphrase(value);
+
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(IsInvalidLdnPassphraseVisible));
+ }
+ }
+
public int Language { get; set; }
public int Region { get; set; }
public int FsGlobalAccessLogMode { get; set; }
@@ -276,6 +293,21 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
+ [GeneratedRegex("Ryujinx-[0-9a-f]{8}")]
+ private static partial Regex LdnPassphraseRegex();
+
+ public bool IsInvalidLdnPassphraseVisible { get; set; }
+
+ public string LdnServer
+ {
+ get => _LdnServer;
+ set
+ {
+ _LdnServer = value;
+ OnPropertyChanged();
+ }
+ }
+
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
{
_virtualFileSystem = virtualFileSystem;
@@ -393,6 +425,11 @@ namespace Ryujinx.Ava.UI.ViewModels
Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(NetworkInterfaceIndex)));
}
+ private bool ValidateLdnPassphrase(string passphrase)
+ {
+ return string.IsNullOrEmpty(passphrase) || (passphrase.Length == 16 && LdnPassphraseRegex().IsMatch(passphrase));
+ }
+
public void ValidateAndSetTimeZone(string location)
{
if (_validTzRegions.Contains(location))
@@ -497,6 +534,9 @@ namespace Ryujinx.Ava.UI.ViewModels
OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value;
MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value;
+ DisableP2P = config.Multiplayer.DisableP2p.Value;
+ LdnPassphrase = config.Multiplayer.LdnPassphrase.Value;
+ LdnServer = config.Multiplayer.LdnServer.Value;
}
public void SaveSettings()
@@ -613,6 +653,9 @@ namespace Ryujinx.Ava.UI.ViewModels
config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]];
config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex;
+ config.Multiplayer.DisableP2p.Value = DisableP2P;
+ config.Multiplayer.LdnPassphrase.Value = LdnPassphrase;
+ config.Multiplayer.LdnServer.Value = LdnServer;
config.ToFileFormat().SaveConfig(Program.ConfigurationPath);
diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
index 1acee3af5..ce4d9fd59 100644
--- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
+++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
@@ -40,8 +40,8 @@ namespace Ryujinx.Ava.UI.Views.Main
private CheckBox[] GenerateToggleFileTypeItems() =>
Enum.GetValues()
.Select(it => (FileName: Enum.GetName(it)!, FileType: it))
- .Select(it =>
- new CheckBox
+ .Select(it =>
+ new CheckBox
{
Content = $".{it.FileName}",
IsChecked = it.FileType.GetConfigValue(ConfigurationState.Instance.UI.ShownFileTypes),
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml
index c7736bf8d..2fc59f04d 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml
+++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml
@@ -1,4 +1,4 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs
index b771933eb..c69307522 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs
+++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml.cs
@@ -1,12 +1,29 @@
using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Ryujinx.Ava.UI.ViewModels;
+using System;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsNetworkView : UserControl
{
+ public SettingsViewModel ViewModel;
+
public SettingsNetworkView()
{
InitializeComponent();
}
+
+ private void GenLdnPassButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ byte[] code = new byte[4];
+ new Random().NextBytes(code);
+ ViewModel.LdnPassphrase = $"Ryujinx-{BitConverter.ToUInt32(code):x8}";
+ }
+
+ private void ClearLdnPassButton_OnClick(object sender, RoutedEventArgs e)
+ {
+ ViewModel.LdnPassphrase = "";
+ }
}
}
diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
index 24a1b62a2..a43c29518 100644
--- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
@@ -154,6 +154,36 @@ namespace Ryujinx.Ava.UI.Windows
});
}
+ private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ var ldnGameDataArray = e.LdnData;
+ ViewModel.LastLdnGameData = ldnGameDataArray;
+ foreach (var application in ViewModel.Applications)
+ {
+ UpdateApplicationWithLdnData(application);
+ }
+ ViewModel.RefreshView();
+ });
+ }
+
+ private void UpdateApplicationWithLdnData(ApplicationData application)
+ {
+ if (application.ControlHolder.ByteSpan.Length > 0 && ViewModel.LastLdnGameData != null)
+ {
+ IEnumerable ldnGameData = ViewModel.LastLdnGameData.Where(game => application.ControlHolder.Value.LocalCommunicationId.Items.Contains(Convert.ToUInt64(game.TitleId, 16)));
+
+ application.PlayerCount = ldnGameData.Sum(game => game.PlayerCount);
+ application.GameCount = ldnGameData.Count();
+ }
+ else
+ {
+ application.PlayerCount = 0;
+ application.GameCount = 0;
+ }
+ }
+
public void Application_Opened(object sender, ApplicationOpenedEventArgs args)
{
if (args.Application != null)
@@ -450,7 +480,20 @@ namespace Ryujinx.Ava.UI.Windows
.Connect()
.ObserveOn(SynchronizationContext.Current!)
.Bind(ViewModel.Applications)
+ .OnItemAdded(UpdateApplicationWithLdnData)
.Subscribe();
+ ApplicationLibrary.LdnGameDataReceived += ApplicationLibrary_LdnGameDataReceived;
+
+ ConfigurationState.Instance.Multiplayer.Mode.Event += (sender, evt) =>
+ {
+ _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn);
+ };
+
+ ConfigurationState.Instance.Multiplayer.LdnServer.Event += (sender, evt) =>
+ {
+ _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn);
+ };
+ _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn);
ViewModel.RefreshFirmwareStatus();
@@ -459,7 +502,7 @@ namespace Ryujinx.Ava.UI.Windows
{
LoadApplications();
}
-
+
_ = CheckLaunchState();
}
@@ -588,13 +631,26 @@ namespace Ryujinx.Ava.UI.Windows
{
switch (fileType)
{
- case "NSP": ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle(); break;
- case "PFS0": ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle(); break;
- case "XCI": ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle(); break;
- case "NCA": ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle(); break;
- case "NRO": ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle(); break;
- case "NSO": ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle(); break;
- default: throw new ArgumentOutOfRangeException(fileType);
+ case "NSP":
+ ConfigurationState.Instance.UI.ShownFileTypes.NSP.Toggle();
+ break;
+ case "PFS0":
+ ConfigurationState.Instance.UI.ShownFileTypes.PFS0.Toggle();
+ break;
+ case "XCI":
+ ConfigurationState.Instance.UI.ShownFileTypes.XCI.Toggle();
+ break;
+ case "NCA":
+ ConfigurationState.Instance.UI.ShownFileTypes.NCA.Toggle();
+ break;
+ case "NRO":
+ ConfigurationState.Instance.UI.ShownFileTypes.NRO.Toggle();
+ break;
+ case "NSO":
+ ConfigurationState.Instance.UI.ShownFileTypes.NSO.Toggle();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(fileType);
}
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs
index 1a177d182..d8c88bed8 100644
--- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs
@@ -80,6 +80,7 @@ namespace Ryujinx.Ava.UI.Windows
NavPanel.Content = AudioPage;
break;
case "NetworkPage":
+ NetworkPage.ViewModel = ViewModel;
NavPanel.Content = NetworkPage;
break;
case "LoggingPage":
diff --git a/src/Ryujinx/UI/Windows/StyleableWindow.cs b/src/Ryujinx/UI/Windows/StyleableWindow.cs
index 493214ee2..9e4eed2e4 100644
--- a/src/Ryujinx/UI/Windows/StyleableWindow.cs
+++ b/src/Ryujinx/UI/Windows/StyleableWindow.cs
@@ -17,7 +17,7 @@ namespace Ryujinx.Ava.UI.Windows
LocaleManager.Instance.LocaleChanged += LocaleChanged;
LocaleChanged();
-
+
Icon = MainWindowViewModel.IconBitmap;
}