diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4fd079af6..009430f92 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -24,6 +24,7 @@
+
diff --git a/README.md b/README.md
index 56333278f..b2a6646f5 100644
--- a/README.md
+++ b/README.md
@@ -141,4 +141,5 @@ See [LICENSE.txt](LICENSE.txt) and [THIRDPARTY.md](distribution/legal/THIRDPARTY
- [LibHac](https://github.com/Thealexbarney/LibHac) is used for our file-system.
- [AmiiboAPI](https://www.amiiboapi.com) is used in our Amiibo emulation.
+- [ldn_mitm](https://github.com/spacemeowx2/ldn_mitm) is used for one of our available multiplayer modes.
- [ShellLink](https://github.com/securifybv/ShellLink) is used for Windows shortcut generation.
diff --git a/distribution/legal/THIRDPARTY.md b/distribution/legal/THIRDPARTY.md
index b0bd5a690..5caa03771 100644
--- a/distribution/legal/THIRDPARTY.md
+++ b/distribution/legal/THIRDPARTY.md
@@ -710,4 +710,4 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
-
\ No newline at end of file
+
diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs
index c473cf562..cd066efba 100644
--- a/src/Ryujinx.Ava/AppHost.cs
+++ b/src/Ryujinx.Ava/AppHost.cs
@@ -190,6 +190,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
+ ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
ConfigurationState.Instance.Multiplayer.Mode.Event += UpdateMultiplayerModeState;
@@ -408,6 +409,11 @@ namespace Ryujinx.Ava
});
}
+ private void UpdateEnableInternetAccessState(object sender, ReactiveEventArgs e)
+ {
+ Device.Configuration.EnableInternetAccess = e.NewValue;
+ }
+
private void UpdateLanInterfaceIdState(object sender, ReactiveEventArgs e)
{
Device.Configuration.MultiplayerLanInterfaceId = e.NewValue;
diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json
index a67b796bd..62aac1227 100644
--- a/src/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -650,7 +650,7 @@
"UserEditorTitle": "Edit User",
"UserEditorTitleCreate": "Create User",
"SettingsTabNetworkInterface": "Network Interface:",
- "NetworkInterfaceTooltip": "The network interface used for LAN features",
+ "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features",
"NetworkInterfaceDefault": "Default",
"PackagingShaders": "Packaging Shaders",
"AboutChangelogButton": "View Changelog on GitHub",
diff --git a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
index 167429433..05108716d 100644
--- a/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
+++ b/src/Ryujinx.Common/Configuration/Multiplayer/MultiplayerMode.cs
@@ -3,5 +3,6 @@
public enum MultiplayerMode
{
Disabled,
+ LdnMitm,
}
}
diff --git a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
index 78fb342b1..3b64a28f5 100644
--- a/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
+++ b/src/Ryujinx.Common/Utilities/NetworkHelpers.cs
@@ -74,5 +74,10 @@ namespace Ryujinx.Common.Utilities
{
return ConvertIpv4Address(IPAddress.Parse(ipAddress));
}
+
+ public static IPAddress ConvertUint(uint ipAddress)
+ {
+ return new IPAddress(new byte[] { (byte)((ipAddress >> 24) & 0xFF), (byte)((ipAddress >> 16) & 0xFF), (byte)((ipAddress >> 8) & 0xFF), (byte)(ipAddress & 0xFF) });
+ }
}
}
diff --git a/src/Ryujinx.HLE/HLEConfiguration.cs b/src/Ryujinx.HLE/HLEConfiguration.cs
index b1ba11b59..d52f1815a 100644
--- a/src/Ryujinx.HLE/HLEConfiguration.cs
+++ b/src/Ryujinx.HLE/HLEConfiguration.cs
@@ -101,7 +101,7 @@ namespace Ryujinx.HLE
///
/// Control if the guest application should be told that there is a Internet connection available.
///
- internal readonly bool EnableInternetAccess;
+ public bool EnableInternetAccess { internal get; set; }
///
/// Control LibHac's integrity check level.
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
new file mode 100644
index 000000000..80ea2c9d7
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/LdnConst.cs
@@ -0,0 +1,12 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn
+{
+ static class LdnConst
+ {
+ public const int SsidLengthMax = 0x20;
+ public const int AdvertiseDataSizeMax = 0x180;
+ public const int UserNameBytesMax = 0x20;
+ public const int NodeCountMax = 8;
+ public const int StationCountMax = NodeCountMax - 1;
+ public const int PassphraseLengthMax = 0x40;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
index 4b7241c43..5fb2aca05 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
index c57a7dc45..9d5477931 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
index f33ceaebe..0461e783e 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
@@ -48,7 +48,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Types
{
result[i].Reserved = new Array7();
- if (i < 8)
+ if (i < LdnConst.NodeCountMax)
{
result[i].State = array[i].State;
array[i].State = NodeLatestUpdateFlags.None;
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
index 85a19a875..5939a1394 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
index 72db4d41a..764862508 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
index 1401f5214..3820f936e 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs
@@ -1,4 +1,4 @@
-using Ryujinx.Common.Memory;
+using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.Types
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
index 07bbbeda3..78ebcac82 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
@@ -1,7 +1,6 @@
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
@@ -30,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_parent.NetworkClient.NetworkChange -= NetworkChanged;
}
- private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+ private void NetworkChanged(object sender, NetworkChangeEventArgs e)
{
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
similarity index 81%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
index ff342d27c..81825e977 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/INetworkClient.cs
@@ -1,12 +1,13 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
interface INetworkClient : IDisposable
{
+ bool NeedsRealId { get; }
+
event EventHandler NetworkChange;
void DisconnectNetwork();
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
index 29cc0e1b9..8c6ea66f7 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
@@ -8,7 +8,7 @@ using Ryujinx.Cpu;
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.RyuLdn;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm;
using Ryujinx.Horizon.Common;
using Ryujinx.Memory;
using System;
@@ -395,7 +395,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
else
{
- if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1)
+ if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@@ -546,7 +546,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment?
NetworkConfig networkConfig = context.RequestData.ReadStruct();
- if (networkConfig.IntentId.LocalCommunicationId == -1)
+ if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@@ -555,7 +555,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId);
- if (!isLocalCommunicationIdValid)
+ if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
{
return ResultCode.InvalidObject;
}
@@ -568,13 +568,13 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel);
securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
- if (networkConfig.NodeCountMax <= 8)
+ if (networkConfig.NodeCountMax <= LdnConst.NodeCountMax)
{
if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0)
{
if (securityConfig.SecurityMode <= SecurityMode.Retail)
{
- if (securityConfig.Passphrase.Length <= 0x40)
+ if (securityConfig.Passphrase.Length <= LdnConst.PassphraseLengthMax)
{
if (_state == NetworkState.AccessPoint)
{
@@ -678,7 +678,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
return _nifmResultCode;
}
- if (bufferSize == 0 || bufferSize > 0x180)
+ if (bufferSize == 0 || bufferSize > LdnConst.AdvertiseDataSizeMax)
{
return ResultCode.InvalidArgument;
}
@@ -848,10 +848,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
context.Memory.Read(bufferPosition, networkInfoBytes);
- networkInfo = MemoryMarshal.Cast(networkInfoBytes)[0];
+ networkInfo = MemoryMarshal.Read(networkInfoBytes);
}
- if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1)
+ if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
{
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
@@ -860,7 +860,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
}
bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId);
- if (!isLocalCommunicationIdValid)
+ if (!isLocalCommunicationIdValid && NetworkClient.NeedsRealId)
{
return ResultCode.InvalidObject;
}
@@ -1061,10 +1061,16 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
{
MultiplayerMode mode = context.Device.Configuration.MultiplayerMode;
+
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initializing with multiplayer mode: {mode}");
+
switch (mode)
{
+ case MultiplayerMode.LdnMitm:
+ NetworkClient = new LdnMitmClient(context.Device.Configuration);
+ break;
case MultiplayerMode.Disabled:
- NetworkClient = new DisabledLdnClient();
+ NetworkClient = new LdnDisabledClient();
break;
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
similarity index 87%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
index 75a1e35ff..e5340b4e9 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnDisabledClient.cs
@@ -1,12 +1,13 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
- class DisabledLdnClient : INetworkClient
+ class LdnDisabledClient : INetworkClient
{
+ public bool NeedsRealId => true;
+
public event EventHandler NetworkChange;
public NetworkError Connect(ConnectRequest request)
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs
new file mode 100644
index 000000000..8cfd77acb
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanDiscovery.cs
@@ -0,0 +1,611 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
+{
+ internal class LanDiscovery : IDisposable
+ {
+ private const int DefaultPort = 11452;
+ private const ushort CommonChannel = 6;
+ private const byte CommonLinkLevel = 3;
+ private const byte CommonNetworkType = 2;
+
+ private const int FailureTimeout = 4000;
+
+ private readonly LdnMitmClient _parent;
+ private readonly LanProtocol _protocol;
+ private bool _initialized;
+ private readonly Ssid _fakeSsid;
+ private ILdnTcpSocket _tcp;
+ private LdnProxyUdpServer _udp, _udp2;
+ private readonly List _stations = new();
+ private readonly object _lock = new();
+
+ private readonly AutoResetEvent _apConnected = new(false);
+
+ internal readonly IPAddress LocalAddr;
+ internal readonly IPAddress LocalBroadcastAddr;
+ internal NetworkInfo NetworkInfo;
+
+ public bool IsHost => _tcp is LdnProxyTcpServer;
+
+ private readonly Random _random = new();
+
+ // NOTE: Credit to https://stackoverflow.com/a/39338188
+ private static IPAddress GetBroadcastAddress(IPAddress address, IPAddress mask)
+ {
+ uint ipAddress = BitConverter.ToUInt32(address.GetAddressBytes(), 0);
+ uint ipMaskV4 = BitConverter.ToUInt32(mask.GetAddressBytes(), 0);
+ uint broadCastIpAddress = ipAddress | ~ipMaskV4;
+
+ return new IPAddress(BitConverter.GetBytes(broadCastIpAddress));
+ }
+
+ private static NetworkInfo GetEmptyNetworkInfo()
+ {
+ NetworkInfo networkInfo = new()
+ {
+ NetworkId = new NetworkId
+ {
+ SessionId = new Array16(),
+ },
+ Common = new CommonNetworkInfo
+ {
+ MacAddress = new Array6(),
+ Ssid = new Ssid
+ {
+ Name = new Array33(),
+ },
+ },
+ Ldn = new LdnNetworkInfo
+ {
+ NodeCountMax = LdnConst.NodeCountMax,
+ SecurityParameter = new Array16(),
+ Nodes = new Array8(),
+ AdvertiseData = new Array384(),
+ Reserved4 = new Array140(),
+ },
+ };
+
+ for (int i = 0; i < LdnConst.NodeCountMax; i++)
+ {
+ networkInfo.Ldn.Nodes[i] = new NodeInfo
+ {
+ MacAddress = new Array6(),
+ UserName = new Array33(),
+ Reserved2 = new Array16(),
+ };
+ }
+
+ return networkInfo;
+ }
+
+ public LanDiscovery(LdnMitmClient parent, IPAddress ipAddress, IPAddress ipv4Mask)
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Initialize LanDiscovery using IP: {ipAddress}");
+
+ _parent = parent;
+ LocalAddr = ipAddress;
+ LocalBroadcastAddr = GetBroadcastAddress(ipAddress, ipv4Mask);
+
+ _fakeSsid = new Ssid
+ {
+ Length = LdnConst.SsidLengthMax,
+ };
+ _random.NextBytes(_fakeSsid.Name.AsSpan()[..32]);
+
+ _protocol = new LanProtocol(this);
+ _protocol.Accept += OnConnect;
+ _protocol.SyncNetwork += OnSyncNetwork;
+ _protocol.DisconnectStation += DisconnectStation;
+
+ NetworkInfo = GetEmptyNetworkInfo();
+
+ ResetStations();
+
+ if (!InitUdp())
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Initialize: InitUdp failed.");
+
+ return;
+ }
+
+ _initialized = true;
+ }
+
+ protected void OnSyncNetwork(NetworkInfo info)
+ {
+ bool updated = false;
+
+ lock (_lock)
+ {
+ if (!NetworkInfo.Equals(info))
+ {
+ NetworkInfo = info;
+ updated = true;
+
+ Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"Host IP: {NetworkHelpers.ConvertUint(info.Ldn.Nodes[0].Ipv4Address)}");
+ }
+ }
+
+ if (updated)
+ {
+ _parent.InvokeNetworkChange(info, true);
+ }
+
+ _apConnected.Set();
+ }
+
+ protected void OnConnect(LdnProxyTcpSession station)
+ {
+ lock (_lock)
+ {
+ station.NodeId = LocateEmptyNode();
+
+ if (_stations.Count > LdnConst.StationCountMax || station.NodeId == -1)
+ {
+ station.Disconnect();
+ station.Dispose();
+
+ return;
+ }
+
+ _stations.Add(station);
+
+ UpdateNodes();
+ }
+ }
+
+ public void DisconnectStation(LdnProxyTcpSession station)
+ {
+ if (!station.IsDisposed)
+ {
+ if (station.IsConnected)
+ {
+ station.Disconnect();
+ }
+
+ station.Dispose();
+ }
+
+ lock (_lock)
+ {
+ if (_stations.Remove(station))
+ {
+ NetworkInfo.Ldn.Nodes[station.NodeId] = new NodeInfo()
+ {
+ MacAddress = new Array6(),
+ UserName = new Array33(),
+ Reserved2 = new Array16(),
+ };
+
+ UpdateNodes();
+ }
+ }
+ }
+
+ public bool SetAdvertiseData(byte[] data)
+ {
+ if (data.Length > LdnConst.AdvertiseDataSizeMax)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "AdvertiseData exceeds size limit.");
+
+ return false;
+ }
+
+ data.CopyTo(NetworkInfo.Ldn.AdvertiseData.AsSpan());
+ NetworkInfo.Ldn.AdvertiseDataSize = (ushort)data.Length;
+
+ // NOTE: Otherwise this results in SessionKeepFailed or MasterDisconnected
+ lock (_lock)
+ {
+ if (NetworkInfo.Ldn.Nodes[0].IsConnected == 1)
+ {
+ UpdateNodes(true);
+ }
+ }
+
+ return true;
+ }
+
+ public void InitNetworkInfo()
+ {
+ lock (_lock)
+ {
+ NetworkInfo.Common.MacAddress = GetFakeMac();
+ NetworkInfo.Common.Channel = CommonChannel;
+ NetworkInfo.Common.LinkLevel = CommonLinkLevel;
+ NetworkInfo.Common.NetworkType = CommonNetworkType;
+ NetworkInfo.Common.Ssid = _fakeSsid;
+
+ NetworkInfo.Ldn.Nodes = new Array8();
+
+ for (int i = 0; i < LdnConst.NodeCountMax; i++)
+ {
+ NetworkInfo.Ldn.Nodes[i].NodeId = (byte)i;
+ NetworkInfo.Ldn.Nodes[i].IsConnected = 0;
+ }
+ }
+ }
+
+ protected Array6 GetFakeMac(IPAddress address = null)
+ {
+ address ??= LocalAddr;
+
+ byte[] ip = address.GetAddressBytes();
+
+ var macAddress = new Array6();
+ new byte[] { 0x02, 0x00, ip[0], ip[1], ip[2], ip[3] }.CopyTo(macAddress.AsSpan());
+
+ return macAddress;
+ }
+
+ public bool InitTcp(bool listening, IPAddress address = null, int port = DefaultPort)
+ {
+ Logger.Debug?.PrintMsg(LogClass.ServiceLdn, $"LanDiscovery InitTcp: IP: {address}, listening: {listening}");
+
+ if (_tcp != null)
+ {
+ _tcp.DisconnectAndStop();
+ _tcp.Dispose();
+ _tcp = null;
+ }
+
+ ILdnTcpSocket tcpSocket;
+
+ if (listening)
+ {
+ try
+ {
+ address ??= LocalAddr;
+
+ tcpSocket = new LdnProxyTcpServer(_protocol, address, port);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpServer: {ex}");
+
+ return false;
+ }
+
+ if (!tcpSocket.Start())
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (address == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ tcpSocket = new LdnProxyTcpClient(_protocol, address, port);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyTcpClient: {ex}");
+
+ return false;
+ }
+ }
+
+ _tcp = tcpSocket;
+
+ return true;
+ }
+
+ public bool InitUdp()
+ {
+ _udp?.Stop();
+ _udp2?.Stop();
+
+ try
+ {
+ // NOTE: Linux won't receive any broadcast packets if the socket is not bound to the broadcast address.
+ // Windows only works if bound to localhost or the local address.
+ // See this discussion: https://stackoverflow.com/questions/13666789/receiving-udp-broadcast-packets-on-linux
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ _udp2 = new LdnProxyUdpServer(_protocol, LocalBroadcastAddr, DefaultPort);
+ }
+
+ _udp = new LdnProxyUdpServer(_protocol, LocalAddr, DefaultPort);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to create LdnProxyUdpServer: {ex}");
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public NetworkInfo[] Scan(ushort channel, ScanFilter filter)
+ {
+ _udp.ClearScanResults();
+
+ if (_protocol.SendBroadcast(_udp, LanPacketType.Scan, DefaultPort) < 0)
+ {
+ return Array.Empty();
+ }
+
+ List outNetworkInfo = new();
+
+ foreach (KeyValuePair item in _udp.GetScanResults())
+ {
+ bool copy = true;
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.LocalCommunicationId))
+ {
+ copy &= filter.NetworkId.IntentId.LocalCommunicationId == item.Value.NetworkId.IntentId.LocalCommunicationId;
+ }
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.SessionId))
+ {
+ copy &= filter.NetworkId.SessionId.AsSpan().SequenceEqual(item.Value.NetworkId.SessionId.AsSpan());
+ }
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.NetworkType))
+ {
+ copy &= filter.NetworkType == (NetworkType)item.Value.Common.NetworkType;
+ }
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.Ssid))
+ {
+ Span gameSsid = item.Value.Common.Ssid.Name.AsSpan()[item.Value.Common.Ssid.Length..];
+ Span scanSsid = filter.Ssid.Name.AsSpan()[filter.Ssid.Length..];
+ copy &= gameSsid.SequenceEqual(scanSsid);
+ }
+
+ if (filter.Flag.HasFlag(ScanFilterFlag.SceneId))
+ {
+ copy &= filter.NetworkId.IntentId.SceneId == item.Value.NetworkId.IntentId.SceneId;
+ }
+
+ if (copy)
+ {
+ if (item.Value.Ldn.Nodes[0].UserName[0] != 0)
+ {
+ outNetworkInfo.Add(item.Value);
+ }
+ else
+ {
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LanDiscovery Scan: Got empty Username. There might be a timing issue somewhere...");
+ }
+ }
+ }
+
+ return outNetworkInfo.ToArray();
+ }
+
+ protected void ResetStations()
+ {
+ lock (_lock)
+ {
+ foreach (LdnProxyTcpSession station in _stations)
+ {
+ station.Disconnect();
+ station.Dispose();
+ }
+
+ _stations.Clear();
+ }
+ }
+
+ private int LocateEmptyNode()
+ {
+ Array8 nodes = NetworkInfo.Ldn.Nodes;
+
+ for (int i = 1; i < nodes.Length; i++)
+ {
+ if (nodes[i].IsConnected == 0)
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ protected void UpdateNodes(bool forceUpdate = false)
+ {
+ int countConnected = 1;
+
+ foreach (LdnProxyTcpSession station in _stations.Where(station => station.IsConnected))
+ {
+ countConnected++;
+
+ station.OverrideInfo();
+
+ // NOTE: This is not part of the original implementation.
+ NetworkInfo.Ldn.Nodes[station.NodeId] = station.NodeInfo;
+ }
+
+ byte nodeCount = (byte)countConnected;
+
+ bool networkInfoChanged = forceUpdate || NetworkInfo.Ldn.NodeCount != nodeCount;
+
+ NetworkInfo.Ldn.NodeCount = nodeCount;
+
+ foreach (LdnProxyTcpSession station in _stations)
+ {
+ if (station.IsConnected)
+ {
+ if (_protocol.SendPacket(station, LanPacketType.SyncNetwork, SpanHelpers.AsSpan(ref NetworkInfo).ToArray()) < 0)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Failed to send {LanPacketType.SyncNetwork} to station {station.NodeId}");
+ }
+ }
+ }
+
+ if (networkInfoChanged)
+ {
+ _parent.InvokeNetworkChange(NetworkInfo, true);
+ }
+ }
+
+ protected NodeInfo GetNodeInfo(NodeInfo node, UserConfig userConfig, ushort localCommunicationVersion)
+ {
+ uint ipAddress = NetworkHelpers.ConvertIpv4Address(LocalAddr);
+
+ node.MacAddress = GetFakeMac();
+ node.IsConnected = 1;
+ node.UserName = userConfig.UserName;
+ node.LocalCommunicationVersion = localCommunicationVersion;
+ node.Ipv4Address = ipAddress;
+
+ return node;
+ }
+
+ public bool CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig)
+ {
+ if (!InitTcp(true))
+ {
+ return false;
+ }
+
+ InitNetworkInfo();
+
+ NetworkInfo.Ldn.NodeCountMax = networkConfig.NodeCountMax;
+ NetworkInfo.Ldn.SecurityMode = (ushort)securityConfig.SecurityMode;
+
+ NetworkInfo.Common.Channel = networkConfig.Channel == 0 ? (ushort)6 : networkConfig.Channel;
+
+ NetworkInfo.NetworkId.SessionId = new Array16();
+ _random.NextBytes(NetworkInfo.NetworkId.SessionId.AsSpan());
+ NetworkInfo.NetworkId.IntentId = networkConfig.IntentId;
+
+ NetworkInfo.Ldn.Nodes[0] = GetNodeInfo(NetworkInfo.Ldn.Nodes[0], userConfig, networkConfig.LocalCommunicationVersion);
+ NetworkInfo.Ldn.Nodes[0].IsConnected = 1;
+ NetworkInfo.Ldn.NodeCount++;
+
+ _parent.InvokeNetworkChange(NetworkInfo, true);
+
+ return true;
+ }
+
+ public void DestroyNetwork()
+ {
+ if (_tcp != null)
+ {
+ try
+ {
+ _tcp.DisconnectAndStop();
+ }
+ finally
+ {
+ _tcp.Dispose();
+ _tcp = null;
+ }
+ }
+
+ ResetStations();
+ }
+
+ public NetworkError Connect(NetworkInfo networkInfo, UserConfig userConfig, uint localCommunicationVersion)
+ {
+ _apConnected.Reset();
+
+ if (networkInfo.Ldn.NodeCount == 0)
+ {
+ return NetworkError.Unknown;
+ }
+
+ IPAddress address = NetworkHelpers.ConvertUint(networkInfo.Ldn.Nodes[0].Ipv4Address);
+
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"Connecting to host: {address}");
+
+ if (!InitTcp(false, address))
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Could not initialize TCPClient");
+
+ return NetworkError.ConnectNotFound;
+ }
+
+ if (!_tcp.Connect())
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Failed to connect.");
+
+ return NetworkError.ConnectFailure;
+ }
+
+ NodeInfo myNode = GetNodeInfo(new NodeInfo(), userConfig, (ushort)localCommunicationVersion);
+ if (_protocol.SendPacket(_tcp, LanPacketType.Connect, SpanHelpers.AsSpan(ref myNode).ToArray()) < 0)
+ {
+ return NetworkError.Unknown;
+ }
+
+ return _apConnected.WaitOne(FailureTimeout) ? NetworkError.None : NetworkError.ConnectTimeout;
+ }
+
+ public void Dispose()
+ {
+ if (_initialized)
+ {
+ DisconnectAndStop();
+ ResetStations();
+ _initialized = false;
+ }
+
+ _protocol.Accept -= OnConnect;
+ _protocol.SyncNetwork -= OnSyncNetwork;
+ _protocol.DisconnectStation -= DisconnectStation;
+ }
+
+ public void DisconnectAndStop()
+ {
+ if (_udp != null)
+ {
+ try
+ {
+ _udp.Stop();
+ }
+ finally
+ {
+ _udp.Dispose();
+ _udp = null;
+ }
+ }
+
+ if (_udp2 != null)
+ {
+ try
+ {
+ _udp2.Stop();
+ }
+ finally
+ {
+ _udp2.Dispose();
+ _udp2 = null;
+ }
+ }
+
+ if (_tcp != null)
+ {
+ try
+ {
+ _tcp.DisconnectAndStop();
+ }
+ finally
+ {
+ _tcp.Dispose();
+ _tcp = null;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs
new file mode 100644
index 000000000..f22e430bd
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LanProtocol.cs
@@ -0,0 +1,314 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
+{
+ internal class LanProtocol
+ {
+ private const uint LanMagic = 0x11451400;
+
+ public const int BufferSize = 2048;
+ public const int TcpTxBufferSize = 0x800;
+ public const int TcpRxBufferSize = 0x1000;
+ public const int TxBufferSizeMax = 0x2000;
+ public const int RxBufferSizeMax = 0x2000;
+
+ private readonly int _headerSize = Marshal.SizeOf();
+
+ private readonly LanDiscovery _discovery;
+
+ public event Action Accept;
+ public event Action Scan;
+ public event Action ScanResponse;
+ public event Action SyncNetwork;
+ public event Action Connect;
+ public event Action DisconnectStation;
+
+ public LanProtocol(LanDiscovery parent)
+ {
+ _discovery = parent;
+ }
+
+ public void InvokeAccept(LdnProxyTcpSession session)
+ {
+ Accept?.Invoke(session);
+ }
+
+ public void InvokeDisconnectStation(LdnProxyTcpSession session)
+ {
+ DisconnectStation?.Invoke(session);
+ }
+
+ private void DecodeAndHandle(LanPacketHeader header, byte[] data, EndPoint endPoint = null)
+ {
+ switch (header.Type)
+ {
+ case LanPacketType.Scan:
+ // UDP
+ if (_discovery.IsHost)
+ {
+ Scan?.Invoke(endPoint, LanPacketType.ScanResponse, SpanHelpers.AsSpan(ref _discovery.NetworkInfo).ToArray());
+ }
+ break;
+ case LanPacketType.ScanResponse:
+ // UDP
+ ScanResponse?.Invoke(MemoryMarshal.Cast(data)[0]);
+ break;
+ case LanPacketType.SyncNetwork:
+ // TCP
+ SyncNetwork?.Invoke(MemoryMarshal.Cast(data)[0]);
+ break;
+ case LanPacketType.Connect:
+ // TCP Session / Station
+ Connect?.Invoke(MemoryMarshal.Cast(data)[0], endPoint);
+ break;
+ default:
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decode error: Unhandled type {header.Type}");
+ break;
+ }
+ }
+
+ public void Read(scoped ref byte[] buffer, scoped ref int bufferEnd, byte[] data, int offset, int size, EndPoint endPoint = null)
+ {
+ if (endPoint != null && _discovery.LocalAddr.Equals(((IPEndPoint)endPoint).Address))
+ {
+ return;
+ }
+
+ int index = 0;
+ while (index < size)
+ {
+ if (bufferEnd < _headerSize)
+ {
+ int copyable2 = Math.Min(size - index, Math.Min(size, _headerSize - bufferEnd));
+
+ Array.Copy(data, index + offset, buffer, bufferEnd, copyable2);
+
+ index += copyable2;
+ bufferEnd += copyable2;
+ }
+
+ if (bufferEnd >= _headerSize)
+ {
+ LanPacketHeader header = MemoryMarshal.Cast(buffer)[0];
+ if (header.Magic != LanMagic)
+ {
+ bufferEnd = 0;
+
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, $"Invalid magic number in received packet. [magic: {header.Magic}] [EP: {endPoint}]");
+
+ return;
+ }
+
+ int totalSize = _headerSize + header.Length;
+ if (totalSize > BufferSize)
+ {
+ bufferEnd = 0;
+
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Max packet size {BufferSize} exceeded.");
+
+ return;
+ }
+
+ int copyable = Math.Min(size - index, Math.Min(size, totalSize - bufferEnd));
+
+ Array.Copy(data, index + offset, buffer, bufferEnd, copyable);
+
+ index += copyable;
+ bufferEnd += copyable;
+
+ if (totalSize == bufferEnd)
+ {
+ byte[] ldnData = new byte[totalSize - _headerSize];
+ Array.Copy(buffer, _headerSize, ldnData, 0, ldnData.Length);
+
+ if (header.Compressed == 1)
+ {
+ if (Decompress(ldnData, out byte[] decompressedLdnData) != 0)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error:\n {header}, {_headerSize}\n {ldnData}, {ldnData.Length}");
+
+ return;
+ }
+
+ if (decompressedLdnData.Length != header.DecompressLength)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error: length does not match. ({decompressedLdnData.Length} != {header.DecompressLength})");
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"Decompress error data: '{string.Join("", decompressedLdnData.Select(x => (int)x).ToArray())}'");
+
+ return;
+ }
+
+ ldnData = decompressedLdnData;
+ }
+
+ DecodeAndHandle(header, ldnData, endPoint);
+
+ bufferEnd = 0;
+ }
+ }
+ }
+ }
+
+ public int SendBroadcast(ILdnSocket s, LanPacketType type, int port)
+ {
+ return SendPacket(s, type, Array.Empty(), new IPEndPoint(_discovery.LocalBroadcastAddr, port));
+ }
+
+ public int SendPacket(ILdnSocket s, LanPacketType type, byte[] data, EndPoint endPoint = null)
+ {
+ byte[] buf = PreparePacket(type, data);
+
+ return s.SendPacketAsync(endPoint, buf) ? 0 : -1;
+ }
+
+ public int SendPacket(LdnProxyTcpSession s, LanPacketType type, byte[] data)
+ {
+ byte[] buf = PreparePacket(type, data);
+
+ return s.SendAsync(buf) ? 0 : -1;
+ }
+
+ private LanPacketHeader PrepareHeader(LanPacketHeader header, LanPacketType type)
+ {
+ header.Magic = LanMagic;
+ header.Type = type;
+ header.Compressed = 0;
+ header.Length = 0;
+ header.DecompressLength = 0;
+ header.Reserved = new Array2();
+
+ return header;
+ }
+
+ private byte[] PreparePacket(LanPacketType type, byte[] data)
+ {
+ LanPacketHeader header = PrepareHeader(new LanPacketHeader(), type);
+ header.Length = (ushort)data.Length;
+
+ byte[] buf;
+ if (data.Length > 0)
+ {
+ if (Compress(data, out byte[] compressed) == 0)
+ {
+ header.DecompressLength = header.Length;
+ header.Length = (ushort)compressed.Length;
+ header.Compressed = 1;
+
+ buf = new byte[compressed.Length + _headerSize];
+
+ SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0);
+ compressed.CopyTo(buf, _headerSize);
+ }
+ else
+ {
+ buf = new byte[data.Length + _headerSize];
+
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, "Compressing packet data failed.");
+
+ SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0);
+ data.CopyTo(buf, _headerSize);
+ }
+ }
+ else
+ {
+ buf = new byte[_headerSize];
+ SpanHelpers.AsSpan(ref header).ToArray().CopyTo(buf, 0);
+ }
+
+ return buf;
+ }
+
+ private int Compress(byte[] input, out byte[] output)
+ {
+ List outputList = new();
+ int i = 0;
+ int maxCount = 0xFF;
+
+ while (i < input.Length)
+ {
+ byte inputByte = input[i++];
+ int count = 0;
+
+ if (inputByte == 0)
+ {
+ while (i < input.Length && input[i] == 0 && count < maxCount)
+ {
+ count += 1;
+ i++;
+ }
+ }
+
+ if (inputByte == 0)
+ {
+ outputList.Add(0);
+
+ if (outputList.Count == BufferSize)
+ {
+ output = null;
+
+ return -1;
+ }
+
+ outputList.Add((byte)count);
+ }
+ else
+ {
+ outputList.Add(inputByte);
+ }
+ }
+
+ output = outputList.ToArray();
+
+ return i == input.Length ? 0 : -1;
+ }
+
+ private int Decompress(byte[] input, out byte[] output)
+ {
+ List outputList = new();
+ int i = 0;
+
+ while (i < input.Length && outputList.Count < BufferSize)
+ {
+ byte inputByte = input[i++];
+
+ outputList.Add(inputByte);
+
+ if (inputByte == 0)
+ {
+ if (i == input.Length)
+ {
+ output = null;
+
+ return -1;
+ }
+
+ int count = input[i++];
+
+ for (int j = 0; j < count; j++)
+ {
+ if (outputList.Count == BufferSize)
+ {
+ break;
+ }
+
+ outputList.Add(inputByte);
+ }
+ }
+ }
+
+ output = outputList.ToArray();
+
+ return i == input.Length ? 0 : -1;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs
new file mode 100644
index 000000000..068013053
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/LdnMitmClient.cs
@@ -0,0 +1,104 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
+using System;
+using System.Net.NetworkInformation;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm
+{
+ ///
+ /// Client implementation for ldn_mitm
+ ///
+ internal class LdnMitmClient : INetworkClient
+ {
+ public bool NeedsRealId => false;
+
+ public event EventHandler NetworkChange;
+
+ private readonly LanDiscovery _lanDiscovery;
+
+ public LdnMitmClient(HLEConfiguration config)
+ {
+ UnicastIPAddressInformation localIpInterface = NetworkHelpers.GetLocalInterface(config.MultiplayerLanInterfaceId).Item2;
+
+ _lanDiscovery = new LanDiscovery(this, localIpInterface.Address, localIpInterface.IPv4Mask);
+ }
+
+ internal void InvokeNetworkChange(NetworkInfo info, bool connected, DisconnectReason reason = DisconnectReason.None)
+ {
+ NetworkChange?.Invoke(this, new NetworkChangeEventArgs(info, connected: connected, disconnectReason: reason));
+ }
+
+ public NetworkError Connect(ConnectRequest request)
+ {
+ return _lanDiscovery.Connect(request.NetworkInfo, request.UserConfig, request.LocalCommunicationVersion);
+ }
+
+ public NetworkError ConnectPrivate(ConnectPrivateRequest request)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient ConnectPrivate");
+
+ return NetworkError.None;
+ }
+
+ public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
+ {
+ return _lanDiscovery.CreateNetwork(request.SecurityConfig, request.UserConfig, request.NetworkConfig);
+ }
+
+ public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient CreateNetworkPrivate");
+
+ return true;
+ }
+
+ public void DisconnectAndStop()
+ {
+ _lanDiscovery.DisconnectAndStop();
+ }
+
+ public void DisconnectNetwork()
+ {
+ _lanDiscovery.DestroyNetwork();
+ }
+
+ public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient Reject");
+
+ return ResultCode.Success;
+ }
+
+ public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
+ {
+ return _lanDiscovery.Scan(channel, scanFilter);
+ }
+
+ public void SetAdvertiseData(byte[] data)
+ {
+ _lanDiscovery.SetAdvertiseData(data);
+ }
+
+ public void SetGameVersion(byte[] versionString)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetGameVersion");
+ }
+
+ public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
+ {
+ // NOTE: This method is not implemented in ldn_mitm
+ Logger.Stub?.PrintMsg(LogClass.ServiceLdn, "LdnMitmClient SetStationAcceptPolicy");
+ }
+
+ public void Dispose()
+ {
+ _lanDiscovery.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs
new file mode 100644
index 000000000..b6e6cea9e
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnSocket.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Net;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal interface ILdnSocket : IDisposable
+ {
+ bool SendPacketAsync(EndPoint endpoint, byte[] buffer);
+ bool Start();
+ bool Stop();
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs
new file mode 100644
index 000000000..97e3bd627
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/ILdnTcpSocket.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal interface ILdnTcpSocket : ILdnSocket
+ {
+ bool Connect();
+ void DisconnectAndStop();
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs
new file mode 100644
index 000000000..cfe9a8aae
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpClient.cs
@@ -0,0 +1,99 @@
+using Ryujinx.Common.Logging;
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal class LdnProxyTcpClient : NetCoreServer.TcpClient, ILdnTcpSocket
+ {
+ private readonly LanProtocol _protocol;
+ private byte[] _buffer;
+ private int _bufferEnd;
+
+ public LdnProxyTcpClient(LanProtocol protocol, IPAddress address, int port) : base(address, port)
+ {
+ _protocol = protocol;
+ _buffer = new byte[LanProtocol.BufferSize];
+ OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
+ OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
+ OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
+ OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
+ }
+
+ protected override void OnConnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient connected!");
+ }
+
+ protected override void OnReceived(byte[] buffer, long offset, long size)
+ {
+ _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size);
+ }
+
+ public void DisconnectAndStop()
+ {
+ DisconnectAsync();
+
+ while (IsConnected)
+ {
+ Thread.Yield();
+ }
+ }
+
+ public bool SendPacketAsync(EndPoint endPoint, byte[] data)
+ {
+ if (endPoint != null)
+ {
+ Logger.Warning?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTcpClient is sending a packet but endpoint is not null.");
+ }
+
+ if (IsConnecting && !IsConnected)
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPClient needs to connect before sending packets. Waiting...");
+
+ while (IsConnecting && !IsConnected)
+ {
+ Thread.Yield();
+ }
+ }
+
+ return SendAsync(data);
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPClient caught an error with code {error}");
+ }
+
+ protected override void Dispose(bool disposingManagedResources)
+ {
+ DisconnectAndStop();
+ base.Dispose(disposingManagedResources);
+ }
+
+ public override bool Connect()
+ {
+ // TODO: NetCoreServer has a Connect() method, but it currently leads to weird issues.
+ base.ConnectAsync();
+
+ while (IsConnecting)
+ {
+ Thread.Sleep(1);
+ }
+
+ return IsConnected;
+ }
+
+ public bool Start()
+ {
+ throw new InvalidOperationException("Start was called.");
+ }
+
+ public bool Stop()
+ {
+ throw new InvalidOperationException("Stop was called.");
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs
new file mode 100644
index 000000000..0ca12b9f6
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpServer.cs
@@ -0,0 +1,54 @@
+using NetCoreServer;
+using Ryujinx.Common.Logging;
+using System;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal class LdnProxyTcpServer : TcpServer, ILdnTcpSocket
+ {
+ private readonly LanProtocol _protocol;
+
+ public LdnProxyTcpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
+ {
+ _protocol = protocol;
+ OptionReuseAddress = true;
+ OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
+ OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
+
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer created a server for this address: {address}:{port}");
+ }
+
+ protected override TcpSession CreateSession()
+ {
+ return new LdnProxyTcpSession(this, _protocol);
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPServer caught an error with code {error}");
+ }
+
+ protected override void Dispose(bool disposingManagedResources)
+ {
+ Stop();
+ base.Dispose(disposingManagedResources);
+ }
+
+ public bool Connect()
+ {
+ throw new InvalidOperationException("Connect was called.");
+ }
+
+ public void DisconnectAndStop()
+ {
+ Stop();
+ }
+
+ public bool SendPacketAsync(EndPoint endpoint, byte[] buffer)
+ {
+ throw new InvalidOperationException("SendPacketAsync was called.");
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs
new file mode 100644
index 000000000..f30c4b011
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyTcpSession.cs
@@ -0,0 +1,83 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Net;
+using System.Net.Sockets;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal class LdnProxyTcpSession : NetCoreServer.TcpSession
+ {
+ private readonly LanProtocol _protocol;
+
+ internal int NodeId;
+ internal NodeInfo NodeInfo;
+
+ private byte[] _buffer;
+ private int _bufferEnd;
+
+ public LdnProxyTcpSession(LdnProxyTcpServer server, LanProtocol protocol) : base(server)
+ {
+ _protocol = protocol;
+ _protocol.Connect += OnConnect;
+ _buffer = new byte[LanProtocol.BufferSize];
+ OptionSendBufferSize = LanProtocol.TcpTxBufferSize;
+ OptionReceiveBufferSize = LanProtocol.TcpRxBufferSize;
+ OptionSendBufferLimit = LanProtocol.TxBufferSizeMax;
+ OptionReceiveBufferLimit = LanProtocol.RxBufferSizeMax;
+ }
+
+ public void OverrideInfo()
+ {
+ NodeInfo.NodeId = (byte)NodeId;
+ NodeInfo.IsConnected = (byte)(IsConnected ? 1 : 0);
+ }
+
+ protected override void OnConnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession connected!");
+ }
+
+ protected override void OnDisconnected()
+ {
+ Logger.Info?.PrintMsg(LogClass.ServiceLdn, "LdnProxyTCPSession disconnected!");
+
+ _protocol.InvokeDisconnectStation(this);
+ }
+
+ protected override void OnReceived(byte[] buffer, long offset, long size)
+ {
+ _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, this.Socket.RemoteEndPoint);
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession caught an error with code {error}");
+
+ Dispose();
+ }
+
+ protected override void Dispose(bool disposingManagedResources)
+ {
+ _protocol.Connect -= OnConnect;
+ base.Dispose(disposingManagedResources);
+ }
+
+ private void OnConnect(NodeInfo info, EndPoint endPoint)
+ {
+ try
+ {
+ if (endPoint.Equals(this.Socket.RemoteEndPoint))
+ {
+ NodeInfo = info;
+ _protocol.InvokeAccept(this);
+ }
+ }
+ catch (System.ObjectDisposedException)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyTCPSession was disposed. [IP: {NodeInfo.Ipv4Address}]");
+
+ _protocol.InvokeDisconnectStation(this);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs
new file mode 100644
index 000000000..b1519d1ff
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Proxy/LdnProxyUdpServer.cs
@@ -0,0 +1,157 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Proxy
+{
+ internal class LdnProxyUdpServer : NetCoreServer.UdpServer, ILdnSocket
+ {
+ private const long ScanFrequency = 1000;
+
+ private readonly LanProtocol _protocol;
+ private byte[] _buffer;
+ private int _bufferEnd;
+
+ private readonly object _scanLock = new();
+
+ private Dictionary _scanResultsLast = new();
+ private Dictionary _scanResults = new();
+ private readonly AutoResetEvent _scanResponse = new(false);
+ private long _lastScanTime;
+
+ public LdnProxyUdpServer(LanProtocol protocol, IPAddress address, int port) : base(address, port)
+ {
+ _protocol = protocol;
+ _protocol.Scan += HandleScan;
+ _protocol.ScanResponse += HandleScanResponse;
+ _buffer = new byte[LanProtocol.BufferSize];
+ OptionReuseAddress = true;
+ OptionReceiveBufferSize = LanProtocol.RxBufferSizeMax;
+ OptionSendBufferSize = LanProtocol.TxBufferSizeMax;
+
+ Start();
+ }
+
+ protected override Socket CreateSocket()
+ {
+ return new Socket(Endpoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp)
+ {
+ EnableBroadcast = true,
+ };
+ }
+
+ protected override void OnStarted()
+ {
+ ReceiveAsync();
+ }
+
+ protected override void OnReceived(EndPoint endpoint, byte[] buffer, long offset, long size)
+ {
+ _protocol.Read(ref _buffer, ref _bufferEnd, buffer, (int)offset, (int)size, endpoint);
+ ReceiveAsync();
+ }
+
+ protected override void OnError(SocketError error)
+ {
+ Logger.Error?.PrintMsg(LogClass.ServiceLdn, $"LdnProxyUdpServer caught an error with code {error}");
+ }
+
+ protected override void Dispose(bool disposingManagedResources)
+ {
+ _protocol.Scan -= HandleScan;
+ _protocol.ScanResponse -= HandleScanResponse;
+
+ _scanResponse.Dispose();
+
+ base.Dispose(disposingManagedResources);
+ }
+
+ public bool SendPacketAsync(EndPoint endpoint, byte[] data)
+ {
+ return SendAsync(endpoint, data);
+ }
+
+ private void HandleScan(EndPoint endpoint, LanPacketType type, byte[] data)
+ {
+ _protocol.SendPacket(this, type, data, endpoint);
+ }
+
+ private void HandleScanResponse(NetworkInfo info)
+ {
+ Span mac = stackalloc byte[8];
+
+ info.Common.MacAddress.AsSpan().CopyTo(mac);
+
+ lock (_scanLock)
+ {
+ _scanResults[BitConverter.ToUInt64(mac)] = info;
+
+ _scanResponse.Set();
+ }
+ }
+
+ public void ClearScanResults()
+ {
+ // Rate limit scans.
+
+ long timeMs = Stopwatch.GetTimestamp() / (Stopwatch.Frequency / 1000);
+ long delay = ScanFrequency - (timeMs - _lastScanTime);
+
+ if (delay > 0)
+ {
+ Thread.Sleep((int)delay);
+ }
+
+ _lastScanTime = timeMs;
+
+ lock (_scanLock)
+ {
+ var newResults = _scanResultsLast;
+ newResults.Clear();
+
+ _scanResultsLast = _scanResults;
+ _scanResults = newResults;
+
+ _scanResponse.Reset();
+ }
+ }
+
+ public Dictionary GetScanResults()
+ {
+ // NOTE: Try to minimize waiting time for scan results.
+ // After we receive the first response, wait a short time for follow-ups and return.
+ // Responses that were too late to catch will appear in the next scan.
+
+ // ldn_mitm does not do this, but this improves latency for games that expect it to be low (it is on console).
+
+ if (_scanResponse.WaitOne(1000))
+ {
+ // Wait a short while longer in case there are some other responses.
+ Thread.Sleep(33);
+ }
+
+ lock (_scanLock)
+ {
+ var results = new Dictionary();
+
+ foreach (KeyValuePair last in _scanResultsLast)
+ {
+ results[last.Key] = last.Value;
+ }
+
+ foreach (KeyValuePair scan in _scanResults)
+ {
+ results[scan.Key] = scan.Value;
+ }
+
+ return results;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs
new file mode 100644
index 000000000..4cebe414d
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketHeader.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 12)]
+ internal struct LanPacketHeader
+ {
+ public uint Magic;
+ public LanPacketType Type;
+ public byte Compressed;
+ public ushort Length;
+ public ushort DecompressLength;
+ public Array2 Reserved;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs
new file mode 100644
index 000000000..901f00b00
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnMitm/Types/LanPacketType.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnMitm.Types
+{
+ internal enum LanPacketType : byte
+ {
+ Scan,
+ ScanResponse,
+ Connect,
+ SyncNetwork,
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs
similarity index 91%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs
index 1cc09c00d..b379d2680 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/NetworkChangeEventArgs.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
class NetworkChangeEventArgs : EventArgs
{
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
index c190d6ed1..e39c01978 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
@@ -1,7 +1,6 @@
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Ldn.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types;
-using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types;
+using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
@@ -22,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
_parent.NetworkClient.NetworkChange += NetworkChanged;
}
- private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+ private void NetworkChanged(object sender, NetworkChangeEventArgs e)
{
LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs
similarity index 86%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs
index 47e48d0a1..058ce62d0 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectPrivateRequest.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0xBC)]
struct ConnectPrivateRequest
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs
similarity index 84%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs
index 9ff46cccb..136589b2a 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/ConnectRequest.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x4FC)]
struct ConnectRequest
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
similarity index 88%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
index 6e890618c..ec0668884 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointPrivateRequest.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
///
/// Advertise data is appended separately (remaining data in the buffer).
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
similarity index 86%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
index 4efe9165a..eecea5eb0 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/CreateAccessPointRequest.cs
@@ -1,7 +1,7 @@
using Ryujinx.HLE.HOS.Services.Ldn.Types;
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
///
/// Advertise data is appended separately (remaining data in the buffer).
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs
similarity index 80%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs
index 70ebf7e38..cd576e055 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkError.cs
@@ -1,4 +1,4 @@
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
enum NetworkError : int
{
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs
similarity index 71%
rename from src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
rename to src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs
index acb0b36ac..7e0c2a43f 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Types/NetworkErrorMessage.cs
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
-namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types
{
[StructLayout(LayoutKind.Sequential, Size = 0x4)]
struct NetworkErrorMessage
diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
index 5e3aa0eac..f3439cc8f 100644
--- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -27,6 +27,7 @@
+
diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
index 9d2df5f03..9ed8fd8cc 100644
--- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
@@ -571,6 +571,7 @@ namespace Ryujinx.Ui.Common.Configuration
{
LanInterfaceId = new ReactiveObject();
Mode = new ReactiveObject();
+ Mode.Event += static (_, e) => LogValueChange(e, nameof(MultiplayerMode));
}
}
diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs
index f4817277d..a9d4be109 100644
--- a/src/Ryujinx/Ui/MainWindow.cs
+++ b/src/Ryujinx/Ui/MainWindow.cs
@@ -1121,6 +1121,14 @@ namespace Ryujinx.Ui
Graphics.Gpu.GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE;
}
+ public void UpdateInternetAccess()
+ {
+ if (_gameLoaded)
+ {
+ _emulationContext.Configuration.EnableInternetAccess = ConfigurationState.Instance.System.EnableInternetAccess.Value;
+ }
+ }
+
public static void SaveConfig()
{
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
index f5186d5c1..dabef14dd 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.cs
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
@@ -671,6 +671,8 @@ namespace Ryujinx.Ui.Windows
}
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
+
+ _parent.UpdateInternetAccess();
MainWindow.UpdateGraphicsConfig();
ThemeHelper.ApplyTheme();
}
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
index fcc8c1d19..f0dbd6b63 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.glade
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
@@ -2993,6 +2993,7 @@
Disabled
- Disabled
+ - ldn_mitm
@@ -3064,7 +3065,7 @@
@@ -3079,7 +3080,7 @@