From 4ca1cab5f8c46e77cb6374bc4767812f1ea503ee Mon Sep 17 00:00:00 2001 From: Vudjun Date: Fri, 25 Oct 2024 23:04:42 +0100 Subject: [PATCH 01/12] RyuLDN implementation The network implementation originates from Berry's public TCP RyuLDN fork. Logo and unrelated changes have been removed. Additionally displays LDN game status in the game selection window when RyuLDN is enabled. --- Directory.Packages.props | 1 + .../Multiplayer/MultiplayerMode.cs | 1 + .../Memory/StructArrayHelpers.cs | 24 +- .../Utilities/NetworkHelpers.cs | 6 + src/Ryujinx.Graphics.GAL/IRenderer.cs | 2 +- src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs | 2 +- src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 2 +- src/Ryujinx.HLE/HLEConfiguration.cs | 16 +- .../HOS/Services/Ldn/Types/NetworkConfig.cs | 2 +- .../HOS/Services/Ldn/Types/ScanFilter.cs | 2 +- .../HOS/Services/Ldn/Types/SecurityConfig.cs | 2 +- .../Services/Ldn/Types/SecurityParameter.cs | 2 +- .../HOS/Services/Ldn/Types/UserConfig.cs | 2 +- .../Ldn/UserServiceCreator/AccessPoint.cs | 2 + .../Ldn/UserServiceCreator/INetworkClient.cs | 1 + .../IUserLocalCommunicationService.cs | 56 +- .../UserServiceCreator/LdnDisabledClient.cs | 7 + .../LdnMitm/LdnMitmClient.cs | 1 + .../UserServiceCreator/LdnRyu/IProxyClient.cs | 7 + .../LdnRyu/LdnMasterProxyClient.cs | 602 +++++++++++++ .../LdnRyu/NetworkTimeout.cs | 83 ++ .../LdnRyu/Proxy/EphemeralPortPool.cs | 53 ++ .../LdnRyu/Proxy/LdnProxy.cs | 254 ++++++ .../LdnRyu/Proxy/LdnProxySocket.cs | 792 ++++++++++++++++++ .../LdnRyu/Proxy/P2pProxyClient.cs | 93 ++ .../LdnRyu/Proxy/P2pProxyServer.cs | 388 +++++++++ .../LdnRyu/Proxy/P2pProxySession.cs | 90 ++ .../LdnRyu/Proxy/ProxyHelpers.cs | 24 + .../LdnRyu/RyuLdnProtocol.cs | 380 +++++++++ .../LdnRyu/Types/DisconnectMessage.cs | 10 + .../LdnRyu/Types/ExternalProxyConfig.cs | 19 + .../Types/ExternalProxyConnectionState.cs | 18 + .../LdnRyu/Types/ExternalProxyToken.cs | 20 + .../LdnRyu/Types/InitializeMessage.cs | 20 + .../LdnRyu/Types/LdnHeader.cs | 13 + .../LdnRyu/Types/PacketId.cs | 36 + .../LdnRyu/Types/PassphraseMessage.cs | 11 + .../LdnRyu/Types/PingMessage.cs | 11 + .../LdnRyu/Types/ProxyConnectRequest.cs | 10 + .../LdnRyu/Types/ProxyConnectResponse.cs | 10 + .../LdnRyu/Types/ProxyDataHeader.cs | 14 + .../LdnRyu/Types/ProxyDataPacket.cs | 8 + .../LdnRyu/Types/ProxyDisconnectMessage.cs | 11 + .../LdnRyu/Types/ProxyInfo.cs | 20 + .../LdnRyu/Types/RejectRequest.cs | 18 + .../LdnRyu/Types/RyuNetworkConfig.cs | 23 + .../LdnRyu/Types/SetAcceptPolicyRequest.cs | 11 + .../Ldn/UserServiceCreator/Station.cs | 2 + .../Types/CreateAccessPointPrivateRequest.cs | 3 + .../Types/CreateAccessPointRequest.cs | 5 +- .../UserServiceCreator/Types/ProxyConfig.cs | 11 + .../HOS/Services/Sockets/Bsd/IClient.cs | 2 +- .../Sockets/Bsd/Impl/ManagedSocket.cs | 19 +- .../Bsd/Impl/ManagedSocketPollManager.cs | 159 ++-- .../Sockets/Bsd/Proxy/DefaultSocket.cs | 178 ++++ .../HOS/Services/Sockets/Bsd/Proxy/ISocket.cs | 47 ++ .../Sockets/Bsd/Proxy/SocketHelpers.cs | 71 ++ .../SslService/SslManagedSocketConnection.cs | 3 +- .../Loaders/Processes/ProcessResult.cs | 4 +- src/Ryujinx.HLE/Ryujinx.HLE.csproj | 1 + src/Ryujinx.Headless.SDL2/Program.cs | 4 +- src/Ryujinx.UI.Common/App/ApplicationData.cs | 2 + .../App/ApplicationLibrary.cs | 41 + src/Ryujinx.UI.Common/App/LdnGameData.cs | 16 + .../App/LdnGameDataReceivedEventArgs.cs | 10 + .../App/LdnGameDataSerializerContext.cs | 11 + .../Configuration/ConfigurationFileFormat.cs | 10 + .../Configuration/ConfigurationState.cs | 23 + .../Configuration/UI/GuiColumns.cs | 1 + .../DiscordIntegrationModule.cs | 4 +- src/Ryujinx/App.axaml.cs | 2 +- src/Ryujinx/AppHost.cs | 22 +- src/Ryujinx/Assets/Locales/en_US.json | 14 +- src/Ryujinx/Common/Locale/LocaleManager.cs | 2 +- src/Ryujinx/Program.cs | 14 +- src/Ryujinx/UI/Applet/AvaHostUIHandler.cs | 2 +- .../Applet/AvaloniaDynamicTextInputHandler.cs | 4 +- .../UI/Applet/ControllerAppletDialog.axaml.cs | 4 +- .../UI/Controls/ApplicationListView.axaml | 6 + .../UI/Controls/NavigationDialogHost.axaml.cs | 12 +- src/Ryujinx/UI/Helpers/GlyphValueConverter.cs | 4 +- .../UI/Helpers/MultiplayerInfoConverter.cs | 44 + src/Ryujinx/UI/Helpers/TimeZoneConverter.cs | 8 +- .../UI/Models/StatusUpdatedEventArgs.cs | 2 +- .../UI/ViewModels/MainWindowViewModel.cs | 18 +- .../UI/ViewModels/SettingsViewModel.cs | 32 +- .../Views/Settings/SettingsNetworkView.axaml | 46 + .../Settings/SettingsNetworkView.axaml.cs | 17 + src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 39 + .../UI/Windows/SettingsWindow.axaml.cs | 1 + src/Ryujinx/UI/Windows/StyleableWindow.cs | 2 +- 91 files changed, 3935 insertions(+), 167 deletions(-) create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/IProxyClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/NetworkTimeout.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/EphemeralPortPool.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxy.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyClient.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxySession.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/ProxyHelpers.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/RyuLdnProtocol.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/DisconnectMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConfig.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyConnectionState.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ExternalProxyToken.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/InitializeMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/LdnHeader.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PacketId.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PassphraseMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/PingMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectRequest.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyConnectResponse.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataHeader.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDataPacket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyDisconnectMessage.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/ProxyInfo.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RejectRequest.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/RyuNetworkConfig.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Types/SetAcceptPolicyRequest.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ProxyConfig.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/DefaultSocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/ISocket.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs create mode 100644 src/Ryujinx.UI.Common/App/LdnGameData.cs create mode 100644 src/Ryujinx.UI.Common/App/LdnGameDataReceivedEventArgs.cs create mode 100644 src/Ryujinx.UI.Common/App/LdnGameDataSerializerContext.cs create mode 100644 src/Ryujinx/UI/Helpers/MultiplayerInfoConverter.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index e6be60790..b8e30f955 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 af2cfa17f..e84d18ee9 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -547,7 +547,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..ece36377c 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -164,6 +164,16 @@ 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; } + /// /// An action called when HLE force a refresh of output after docked mode changed. /// @@ -194,7 +204,9 @@ namespace Ryujinx.HLE float audioVolume, bool useHypervisor, string multiplayerLanInterfaceId, - MultiplayerMode multiplayerMode) + MultiplayerMode multiplayerMode, + bool multiplayerDisableP2p, + string multiplayerLdnPassphrase) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -222,6 +234,8 @@ namespace Ryujinx.HLE UseHypervisor = useHypervisor; MultiplayerLanInterfaceId = multiplayerLanInterfaceId; MultiplayerMode = multiplayerMode; + MultiplayerDisableP2p = multiplayerDisableP2p; + MultiplayerLdnPassphrase = multiplayerLdnPassphrase; } } } 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..23abcaaf7 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; 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..e769060a5 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 LanPlayHost = "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,21 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator switch (mode) { + case MultiplayerMode.LdnRyu: + try + { + if (!IPAddress.TryParse(LanPlayHost, out IPAddress ipAddress)) + { + ipAddress = Dns.GetHostEntry(LanPlayHost).AddressList[0]; + } + NetworkClient = new LdnMasterProxyClient(ipAddress.ToString(), LanPlayPort, context.Device.Configuration); + } + catch (Exception) + { + Logger.Error?.Print(LogClass.ServiceLdn, "Could not locate LdnRyu server. Defaulting to stubbed wireless."); + NetworkClient = new LdnDisabledClient(); + } + break; case MultiplayerMode.LdnMitm: NetworkClient = new LdnMitmClient(context.Device.Configuration); break; @@ -1103,7 +1141,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..ec6d636f0 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs @@ -0,0 +1,602 @@ +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)); + + 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)); + + 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..ee4342564 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs @@ -0,0 +1,792 @@ +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); + } + + 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..f06fbbf48 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; 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..85156633e 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; @@ -313,7 +314,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 +348,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 +494,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 +532,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..e14468ca4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs @@ -0,0 +1,71 @@ +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(); + + 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/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 b6ccb2ac4..e9699d409 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -578,7 +578,9 @@ 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 b1e346291..63ecb706d 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..f84ec9f2a 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; @@ -43,6 +46,7 @@ namespace Ryujinx.UI.App.Common { 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 +66,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) { @@ -719,6 +724,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 +781,41 @@ namespace Ryujinx.UI.App.Common } } + public async Task RefreshLdn() + { + + if (ConfigurationState.Instance.Multiplayer.Mode == MultiplayerMode.LdnRyu) + { + try + { + IEnumerable ldnGameDataArray = Array.Empty(); + using HttpClient httpClient = new HttpClient(); + string ldnGameDataArrayString = await httpClient.GetStringAsync("https://ryuldnweb.vudjun.com/api/public_games"); + ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData); + var evt = new LdnGameDataReceivedEventArgs + { + LdnData = ldnGameDataArray + }; + LdnGameDataReceived?.Invoke(null, evt); + } + catch + { + 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."); + 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 b357f0d30..011cc1c98 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -402,6 +402,16 @@ 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; } + /// /// Uses Hypervisor over JIT if available /// diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index b7ad29051..63c3e8479 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -32,6 +32,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; } @@ -45,6 +46,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(); @@ -584,11 +586,24 @@ namespace Ryujinx.UI.Common.Configuration /// public ReactiveObject Mode { get; private set; } + /// + /// Disable P2P Toggle + /// + public ReactiveObject DisableP2p { get; private set; } + + /// + /// Local network passphrase, for private networks. + /// + public ReactiveObject LdnPassphrase { get; private set; } + public MultiplayerSection() { LanInterfaceId = new ReactiveObject(); Mode = new ReactiveObject(); Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode)); + DisableP2p = new ReactiveObject(); + DisableP2p.Event += static (_, e) => LogValueChange(e, nameof(DisableP2p)); + LdnPassphrase = new ReactiveObject(); } } @@ -746,6 +761,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, @@ -797,6 +813,8 @@ namespace Ryujinx.UI.Common.Configuration PreferredGpu = Graphics.PreferredGpu, MultiplayerLanInterfaceId = Multiplayer.LanInterfaceId, MultiplayerMode = Multiplayer.Mode, + MultiplayerDisableP2p = Multiplayer.DisableP2p, + MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, }; return configurationFile; @@ -856,6 +874,8 @@ 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 = ""; UI.GuiColumns.FavColumn.Value = true; UI.GuiColumns.IconColumn.Value = true; UI.GuiColumns.AppColumn.Value = true; @@ -1613,6 +1633,7 @@ namespace Ryujinx.UI.Common.Configuration UI.GuiColumns.AppColumn.Value = configurationFileFormat.GuiColumns.AppColumn; UI.GuiColumns.DevColumn.Value = configurationFileFormat.GuiColumns.DevColumn; UI.GuiColumns.VersionColumn.Value = configurationFileFormat.GuiColumns.VersionColumn; + UI.GuiColumns.LdnInfoColumn.Value = configurationFileFormat.GuiColumns.LdnInfoColumn; UI.GuiColumns.TimePlayedColumn.Value = configurationFileFormat.GuiColumns.TimePlayedColumn; UI.GuiColumns.LastPlayedColumn.Value = configurationFileFormat.GuiColumns.LastPlayedColumn; UI.GuiColumns.FileExtColumn.Value = configurationFileFormat.GuiColumns.FileExtColumn; @@ -1651,6 +1672,8 @@ 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; if (configurationFileUpdated) { diff --git a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs index c778ef1f1..c486492e0 100644 --- a/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs +++ b/src/Ryujinx.UI.Common/Configuration/UI/GuiColumns.cs @@ -7,6 +7,7 @@ namespace Ryujinx.UI.Common.Configuration.UI public bool AppColumn { get; set; } public bool DevColumn { get; set; } public bool VersionColumn { get; set; } + public bool LdnInfoColumn { get; set; } public bool TimePlayedColumn { get; set; } public bool LastPlayedColumn { get; set; } public bool FileExtColumn { get; set; } diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs index 01781bab6..3b4d03a4f 100644 --- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs +++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs @@ -154,11 +154,11 @@ namespace Ryujinx.UI.Common "0100d680194b2000", // Pikmin 2 "0100f4c009322000", // Pikmin 3 Deluxe "0100b7c00933a000", // Pikmin 4 - + "01004ad014bf0000", // Sonic Frontiers "01005ea01c0fc000", // SONIC X SHADOW GENERATIONS "01005ea01c0fc001", // ^ - + "01004d300c5ae000", // Kirby and the Forgotten Land "01006b601380e000", // Kirby's Return to Dreamland Deluxe "01007e3006dda000", // Kirby Star Allies diff --git a/src/Ryujinx/App.axaml.cs b/src/Ryujinx/App.axaml.cs index 295ac1503..645876ebe 100644 --- a/src/Ryujinx/App.axaml.cs +++ b/src/Ryujinx/App.axaml.cs @@ -88,7 +88,7 @@ namespace Ryujinx.Ava } }); } - + private void CustomThemeChanged_Event(object _, ReactiveEventArgs __) => ApplyConfiguredTheme(); private void ThemeChanged_Event(object _, ReactiveEventArgs __) => ApplyConfiguredTheme(); diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index e2cede1c4..f7f8fe4f9 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -207,6 +207,8 @@ 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.DisableP2p.Event += UpdateDisableP2pState; _gpuCancellationTokenSource = new CancellationTokenSource(); _gpuDoneEvent = new ManualResetEvent(false); @@ -491,6 +493,16 @@ namespace Ryujinx.Ava Device.Configuration.MultiplayerMode = e.NewValue; } + private void UpdateLdnPassphraseState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerLdnPassphrase = e.NewValue; + } + + private void UpdateDisableP2pState(object sender, ReactiveEventArgs e) + { + Device.Configuration.MultiplayerDisableP2p = e.NewValue; + } + public void ToggleVSync() { Device.EnableDeviceVsync = !Device.EnableDeviceVsync; @@ -873,10 +885,10 @@ 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)); } private static IHardwareDeviceDriver InitializeAudio() @@ -1060,7 +1072,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 68b48146b..e63208d59 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -807,5 +807,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/Locale/LocaleManager.cs b/src/Ryujinx/Common/Locale/LocaleManager.cs index 3247a55f8..b57caa468 100644 --- a/src/Ryujinx/Common/Locale/LocaleManager.cs +++ b/src/Ryujinx/Common/Locale/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 5087d5d82..42daa98c1 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -105,7 +105,7 @@ namespace Ryujinx.Ava Console.Title = $"Ryujinx 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(); @@ -231,11 +231,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 @@ -247,13 +245,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 d5c910629..00e83e09d 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 de9d10ddd..586d396a4 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 6c7a080d1..0304f06c0 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" @@ -117,6 +118,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); @@ -156,9 +156,9 @@ namespace Ryujinx.Ava.UI.Controls if (profile == null) { Dispatcher.UIThread.Post(Action); - + return; - + static async void Action() { await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]); 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 05104cf54..7421fd1fa 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -115,6 +115,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")!); @@ -266,7 +268,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool ShowFirmwareStatus => !ShowLoadProgress; - public bool ShowRightmostSeparator + public bool ShowRightmostSeparator { get => _showRightmostSeparator; set @@ -527,7 +529,7 @@ namespace Ryujinx.Ava.UI.ViewModels OnPropertyChanged(); } } - + public string ShaderCountText { get => _shaderCountText; @@ -990,7 +992,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 @@ -1215,7 +1217,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void InitializeGame() { RendererHostControl.WindowCreated += RendererHost_Created; - + AppHost.StatusUpdatedEvent += Update_StatusBar; AppHost.AppExit += AppHost_AppExit; @@ -1264,9 +1266,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; @@ -1666,7 +1668,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 8772b5697..b576c1dba 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,7 @@ namespace Ryujinx.Ava.UI.ViewModels public event Action SaveSettingsEvent; private int _networkInterfaceIndex; private int _multiplayerModeIndex; + private string _ldnPassphrase; public int ResolutionScale { @@ -180,10 +182,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 +292,11 @@ namespace Ryujinx.Ava.UI.ViewModels } } + [GeneratedRegex("Ryujinx-[0-9a-f]{8}")] + private static partial Regex LdnPassphraseRegex(); + + public bool IsInvalidLdnPassphraseVisible { get; set; } + public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() { _virtualFileSystem = virtualFileSystem; @@ -393,6 +414,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 +523,8 @@ 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; } public void SaveSettings() @@ -613,6 +641,8 @@ 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.ToFileFormat().SaveConfig(Program.ConfigurationPath); diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml index 67fac192d..f75d05c05 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml @@ -36,11 +36,57 @@ + + + + + + + + + + + + + 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 b9a901125..6719bcd4c 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -29,6 +29,7 @@ using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Helper; using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Linq; using System.Runtime.Versioning; using System.Threading; @@ -154,6 +155,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) @@ -463,7 +494,15 @@ 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); + }; + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); ViewModel.RefreshFirmwareStatus(); 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; } -- 2.47.1 From 53e066aab761d3722e28eac3893e3af37e40b483 Mon Sep 17 00:00:00 2001 From: Vudjun Date: Sat, 26 Oct 2024 21:47:41 +0100 Subject: [PATCH 02/12] Fix some connection issues by always sending NetworkChangeEvent immediately --- .../LdnRyu/LdnMasterProxyClient.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs index ec6d636f0..4c7814b8e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/LdnMasterProxyClient.cs @@ -489,6 +489,41 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu 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(); } @@ -575,6 +610,14 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu 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(); } -- 2.47.1 From 3e51a2274253e2adb5ccbe9b3250001a6a53d36e Mon Sep 17 00:00:00 2001 From: Vudjun Date: Sat, 26 Oct 2024 22:03:28 +0100 Subject: [PATCH 03/12] Whitespace fixes --- .../App/ApplicationLibrary.cs | 2 +- .../UI/ViewModels/MainWindowViewModel.cs | 2 +- .../UI/Views/Main/MainMenuBarView.axaml.cs | 4 +-- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 29 ++++++++++++++----- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index f84ec9f2a..b4b3fa4d2 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -692,7 +692,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) ); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 7421fd1fa..f9fc8cbda 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -173,7 +173,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 diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 305bd6b34..a4b388b7c 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -41,8 +41,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/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 6719bcd4c..461a8cad7 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -511,7 +511,7 @@ namespace Ryujinx.Ava.UI.Windows { LoadApplications(); } - + _ = CheckLaunchState(); } @@ -641,13 +641,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); -- 2.47.1 From 89e1f50a345b2eae12925988a7d9708f5f332c4e Mon Sep 17 00:00:00 2001 From: Vudjun Date: Wed, 30 Oct 2024 19:55:21 +0000 Subject: [PATCH 04/12] Fix a crash --- .../HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs index e14468ca4..485a7f86b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Proxy/SocketHelpers.cs @@ -16,7 +16,10 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Proxy 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(); - Socket.Select(readDefault, writeDefault, errorDefault, timeout); + if (readDefault.Count != 0 || writeDefault.Count != 0 || errorDefault.Count != 0) + { + Socket.Select(readDefault, writeDefault, errorDefault, timeout); + } void FilterSockets(List removeFrom, List selectedSockets, Func ldnCheck) { -- 2.47.1 From ab3c84f099f09b3bcd1ae0dfed8793e02ebcb905 Mon Sep 17 00:00:00 2001 From: Vudjun Date: Thu, 31 Oct 2024 09:20:07 +0000 Subject: [PATCH 05/12] Fix binding to ephemeral ports --- .../Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs | 5 +++++ .../HOS/Services/Ldn/UserServiceCreator/Station.cs | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) 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 index ee4342564..ed7a9c751 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/LdnProxySocket.cs @@ -256,6 +256,11 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy { _proxy.ReturnEphemeralPort(ProtocolType, (ushort)((IPEndPoint)LocalEndPoint).Port); } + var asIPEndpoint = (IPEndPoint)localEP; + if (asIPEndpoint.Port == 0) + { + asIPEndpoint.Port = (ushort)_proxy.GetEphemeralPort(ProtocolType); + } LocalEndPoint = (IPEndPoint)localEP; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs index f06fbbf48..fa43f789e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs @@ -50,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) -- 2.47.1 From 825de3d23886df2669ff2e3a1e0e36853e06951a Mon Sep 17 00:00:00 2001 From: Vudjun Date: Mon, 4 Nov 2024 18:32:14 +0000 Subject: [PATCH 06/12] Add the ability to set a custom LDN server --- src/Ryujinx.HLE/HLEConfiguration.cs | 9 ++++++++- .../IUserLocalCommunicationService.cs | 14 ++++++++++---- src/Ryujinx.UI.Common/App/ApplicationLibrary.cs | 12 +++++++++--- .../Configuration/ConfigurationFileFormat.cs | 5 +++++ .../Configuration/ConfigurationState.cs | 9 +++++++++ src/Ryujinx/AppHost.cs | 9 ++++++++- src/Ryujinx/Assets/Locales/en_US.json | 6 +++++- src/Ryujinx/UI/ViewModels/SettingsViewModel.cs | 13 +++++++++++++ .../UI/Views/Settings/SettingsNetworkView.axaml | 13 ++++++++++++- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 5 +++++ 10 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs index ece36377c..70fcf278d 100644 --- a/src/Ryujinx.HLE/HLEConfiguration.cs +++ b/src/Ryujinx.HLE/HLEConfiguration.cs @@ -174,6 +174,11 @@ namespace Ryujinx.HLE /// 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. /// @@ -206,7 +211,8 @@ namespace Ryujinx.HLE string multiplayerLanInterfaceId, MultiplayerMode multiplayerMode, bool multiplayerDisableP2p, - string multiplayerLdnPassphrase) + string multiplayerLdnPassphrase, + string multiplayerLdnServer) { VirtualFileSystem = virtualFileSystem; LibHacHorizonManager = libHacHorizonManager; @@ -236,6 +242,7 @@ namespace Ryujinx.HLE MultiplayerMode = multiplayerMode; MultiplayerDisableP2p = multiplayerDisableP2p; MultiplayerLdnPassphrase = multiplayerLdnPassphrase; + MultiplayerLdnServer = multiplayerLdnServer; } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index e769060a5..9f65aed4b 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -23,7 +23,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator { class IUserLocalCommunicationService : IpcService, IDisposable { - public static string LanPlayHost = "ryuldn.vudjun.com"; + public static string DefaultLanPlayHost = "ryuldn.vudjun.com"; public static short LanPlayPort = 30456; public INetworkClient NetworkClient { get; private set; } @@ -1092,15 +1092,21 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator case MultiplayerMode.LdnRyu: try { - if (!IPAddress.TryParse(LanPlayHost, out IPAddress ipAddress)) + string ldnServer = context.Device.Configuration.MultiplayerLdnServer; + if (string.IsNullOrEmpty(ldnServer)) { - ipAddress = Dns.GetHostEntry(LanPlayHost).AddressList[0]; + 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) + 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; diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index b4b3fa4d2..174db51ad 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -44,6 +44,7 @@ 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; @@ -788,9 +789,14 @@ namespace Ryujinx.UI.App.Common { 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://ryuldnweb.vudjun.com/api/public_games"); + string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games"); ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData); var evt = new LdnGameDataReceivedEventArgs { @@ -798,9 +804,9 @@ namespace Ryujinx.UI.App.Common }; LdnGameDataReceived?.Invoke(null, evt); } - catch + 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."); + 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() diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs index c2dd6a710..80ba1b186 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs @@ -402,6 +402,11 @@ namespace Ryujinx.UI.Common.Configuration /// 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.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 6b3c8db3b..c493bd34f 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -584,6 +584,11 @@ namespace Ryujinx.UI.Common.Configuration /// public ReactiveObject LdnPassphrase { get; private set; } + /// + /// Custom LDN server + /// + public ReactiveObject LdnServer { get; private set; } + public MultiplayerSection() { LanInterfaceId = new ReactiveObject(); @@ -592,6 +597,7 @@ namespace Ryujinx.UI.Common.Configuration DisableP2p = new ReactiveObject(); DisableP2p.Event += static (_, e) => LogValueChange(e, nameof(DisableP2p)); LdnPassphrase = new ReactiveObject(); + LdnServer = new ReactiveObject(); } } @@ -801,6 +807,7 @@ namespace Ryujinx.UI.Common.Configuration MultiplayerMode = Multiplayer.Mode, MultiplayerDisableP2p = Multiplayer.DisableP2p, MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, + LdnServer = Multiplayer.LdnServer, }; return configurationFile; @@ -862,6 +869,7 @@ namespace Ryujinx.UI.Common.Configuration 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; @@ -1656,6 +1664,7 @@ namespace Ryujinx.UI.Common.Configuration 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/AppHost.cs b/src/Ryujinx/AppHost.cs index a6abe84d8..7246be4b9 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -208,6 +208,7 @@ namespace Ryujinx.Ava 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(); @@ -498,6 +499,11 @@ namespace Ryujinx.Ava 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; @@ -878,7 +884,8 @@ namespace Ryujinx.Ava ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value, ConfigurationState.Instance.Multiplayer.Mode, ConfigurationState.Instance.Multiplayer.DisableP2p, - ConfigurationState.Instance.Multiplayer.LdnPassphrase)); + ConfigurationState.Instance.Multiplayer.LdnPassphrase, + ConfigurationState.Instance.Multiplayer.LdnServer)); } private static IHardwareDeviceDriver InitializeAudio() diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index e63208d59..de30fd63f 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -819,5 +819,9 @@ "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>\"" + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"", + "LdnServer": "Custom LDN Server:", + "LdnServerTooltip": "The LDN server to use for LDN connections. Leave blank to use the default server.", + "LdnServerInputTooltip": "Enter the URL of the LDN server to use.", + "LdnServerInputDefault": "(default)" } diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 6896e56af..2da252d00 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -58,6 +58,7 @@ namespace Ryujinx.Ava.UI.ViewModels private int _networkInterfaceIndex; private int _multiplayerModeIndex; private string _ldnPassphrase; + private string _LdnServer; public int ResolutionScale { @@ -297,6 +298,16 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsInvalidLdnPassphraseVisible { get; set; } + public string LdnServer + { + get => _LdnServer; + set + { + _LdnServer = value; + OnPropertyChanged(); + } + } + public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() { _virtualFileSystem = virtualFileSystem; @@ -525,6 +536,7 @@ namespace Ryujinx.Ava.UI.ViewModels MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; DisableP2P = config.Multiplayer.DisableP2p.Value; LdnPassphrase = config.Multiplayer.LdnPassphrase.Value; + LdnServer = config.Multiplayer.LdnServer.Value; } public void SaveSettings() @@ -643,6 +655,7 @@ namespace Ryujinx.Ava.UI.ViewModels 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/Settings/SettingsNetworkView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml index d23bbbccc..79c1e90f0 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/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 9416cf477..298ef2d55 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -501,6 +501,11 @@ namespace Ryujinx.Ava.UI.Windows { _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); }; + + ConfigurationState.Instance.Multiplayer.LdnServer.Event += (sender, evt) => + { + _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); + }; _ = Task.Run(ViewModel.ApplicationLibrary.RefreshLdn); ViewModel.RefreshFirmwareStatus(); -- 2.47.1 From bb81c9dcd9ac19df237182e605ff1885243a505b Mon Sep 17 00:00:00 2001 From: Vudjun Date: Mon, 4 Nov 2024 18:40:43 +0000 Subject: [PATCH 07/12] Add missing parameter in headless build --- src/Ryujinx.Headless.SDL2/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index e9699d409..3713d035c 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -580,6 +580,7 @@ namespace Ryujinx.Headless.SDL2 options.MultiplayerLanInterfaceId, Common.Configuration.Multiplayer.MultiplayerMode.Disabled, false, + "", ""); return new Switch(configuration); -- 2.47.1 From 632676db44281e068791c922ab46031043d0c5cd Mon Sep 17 00:00:00 2001 From: Vudjun Date: Mon, 4 Nov 2024 23:11:16 +0000 Subject: [PATCH 08/12] Correct tooltip for LDN server input --- src/Ryujinx/Assets/Locales/en_US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index de30fd63f..43c81e7a4 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -822,6 +822,6 @@ "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"", "LdnServer": "Custom LDN Server:", "LdnServerTooltip": "The LDN server to use for LDN connections. Leave blank to use the default server.", - "LdnServerInputTooltip": "Enter the URL of the LDN server to use.", + "LdnServerInputTooltip": "Enter the name or IP address of the LDN server to use.", "LdnServerInputDefault": "(default)" } -- 2.47.1 From 475b721e7d0426994401d18dd1a2ff1e52bd3220 Mon Sep 17 00:00:00 2001 From: Vudjun Date: Wed, 6 Nov 2024 20:49:39 +0000 Subject: [PATCH 09/12] Always allow localhost resolution --- src/Ryujinx.HLE/HOS/Services/Sockets/Sfdnsres/IResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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}"); -- 2.47.1 From cc271ddceac437a28fc61be8ea889716616d2819 Mon Sep 17 00:00:00 2001 From: Vudjun Date: Wed, 6 Nov 2024 20:49:58 +0000 Subject: [PATCH 10/12] Add a warning if game is trying to use blocking sockets --- .../HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 85156633e..981fe0a8f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sockets/Bsd/Impl/ManagedSocket.cs @@ -186,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; @@ -200,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; @@ -237,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; -- 2.47.1 From f80dc6d9ab247cb75fa83a119bfa40b34a80874f Mon Sep 17 00:00:00 2001 From: Vudjun Date: Fri, 8 Nov 2024 17:35:36 +0000 Subject: [PATCH 11/12] Remove LDN Server input box from configuration --- src/Ryujinx/Assets/Locales/en_US.json | 6 +----- .../UI/Views/Settings/SettingsNetworkView.axaml | 11 ----------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 760343b90..3ed614cc8 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -859,9 +859,5 @@ "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>\"", - "LdnServer": "Custom LDN Server:", - "LdnServerTooltip": "The LDN server to use for LDN connections. Leave blank to use the default server.", - "LdnServerInputTooltip": "Enter the name or IP address of the LDN server to use.", - "LdnServerInputDefault": "(default)" + "InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\"" } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml index 79c1e90f0..2fc59f04d 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml @@ -87,17 +87,6 @@ IsVisible="{Binding IsInvalidLdnPassphraseVisible}" Focusable="False" Text="{ext:Locale InvalidLdnPassphrase}" /> - - - - -- 2.47.1 From 4845dde5a63f88554fe8afe742f440fbc99cfbc2 Mon Sep 17 00:00:00 2001 From: Vudjun Date: Sun, 10 Nov 2024 18:25:11 +0000 Subject: [PATCH 12/12] Fix a crash that could happen when hosting a game if the server disconnects. --- .../HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs index 23abcaaf7..bd00a3139 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs @@ -26,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) -- 2.47.1