aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator
diff options
context:
space:
mode:
Diffstat (limited to 'src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator')
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs104
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs1060
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs15
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs16
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs62
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs24
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs24
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs16
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs18
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs22
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs10
-rw-r--r--src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs115
12 files changed, 1461 insertions, 25 deletions
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
new file mode 100644
index 00000000..07bbbeda
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/AccessPoint.cs
@@ -0,0 +1,104 @@
+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 System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
+{
+ class AccessPoint : IDisposable
+ {
+ private byte[] _advertiseData;
+
+ private readonly IUserLocalCommunicationService _parent;
+
+ public NetworkInfo NetworkInfo;
+ public Array8<NodeLatestUpdate> LatestUpdates = new();
+ public bool Connected { get; private set; }
+
+ public AccessPoint(IUserLocalCommunicationService parent)
+ {
+ _parent = parent;
+
+ _parent.NetworkClient.NetworkChange += NetworkChanged;
+ }
+
+ public void Dispose()
+ {
+ _parent.NetworkClient.DisconnectNetwork();
+
+ _parent.NetworkClient.NetworkChange -= NetworkChanged;
+ }
+
+ private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+ {
+ LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
+
+ NetworkInfo = e.Info;
+
+ if (Connected != e.Connected)
+ {
+ Connected = e.Connected;
+
+ if (Connected)
+ {
+ _parent.SetState(NetworkState.AccessPointCreated);
+ }
+ else
+ {
+ _parent.SetDisconnectReason(e.DisconnectReasonOrDefault(DisconnectReason.DestroyedBySystem));
+ }
+ }
+ else
+ {
+ _parent.SetState();
+ }
+ }
+
+ public ResultCode SetAdvertiseData(byte[] advertiseData)
+ {
+ _advertiseData = advertiseData;
+
+ _parent.NetworkClient.SetAdvertiseData(_advertiseData);
+
+ return ResultCode.Success;
+ }
+
+ public ResultCode SetStationAcceptPolicy(AcceptPolicy acceptPolicy)
+ {
+ _parent.NetworkClient.SetStationAcceptPolicy(acceptPolicy);
+
+ return ResultCode.Success;
+ }
+
+ public ResultCode CreateNetwork(SecurityConfig securityConfig, UserConfig userConfig, NetworkConfig networkConfig)
+ {
+ CreateAccessPointRequest request = new()
+ {
+ SecurityConfig = securityConfig,
+ UserConfig = userConfig,
+ NetworkConfig = networkConfig,
+ };
+
+ bool success = _parent.NetworkClient.CreateNetwork(request, _advertiseData ?? Array.Empty<byte>());
+
+ return success ? ResultCode.Success : ResultCode.InvalidState;
+ }
+
+ public ResultCode CreateNetworkPrivate(SecurityConfig securityConfig, SecurityParameter securityParameter, UserConfig userConfig, NetworkConfig networkConfig, AddressList addressList)
+ {
+ CreateAccessPointPrivateRequest request = new()
+ {
+ SecurityConfig = securityConfig,
+ SecurityParameter = securityParameter,
+ UserConfig = userConfig,
+ NetworkConfig = networkConfig,
+ AddressList = addressList,
+ };
+
+ bool success = _parent.NetworkClient.CreateNetworkPrivate(request, _advertiseData);
+
+ return success ? ResultCode.Success : ResultCode.InvalidState;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
index d390a3e6..6abd2b89 100644
--- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs
@@ -1,88 +1,1098 @@
-using Ryujinx.HLE.HOS.Ipc;
+using LibHac.Ns;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration.Multiplayer;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+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.Horizon.Common;
+using Ryujinx.Memory;
using System;
+using System.IO;
using System.Net;
+using System.Net.NetworkInformation;
+using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
{
- class IUserLocalCommunicationService : IpcService
+ class IUserLocalCommunicationService : IpcService, IDisposable
{
- // TODO(Ac_K): Determine what the hardcoded unknown value is.
- private const int UnknownValue = 90;
+ public INetworkClient NetworkClient { get; private set; }
- private readonly NetworkInterface _networkInterface;
+ private const int NifmRequestID = 90;
+ private const string DefaultIPAddress = "127.0.0.1";
+ private const string DefaultSubnetMask = "255.255.255.0";
+ private const bool IsDevelopment = false;
- private int _stateChangeEventHandle = 0;
+ private readonly KEvent _stateChangeEvent;
+
+ private NetworkState _state;
+ private DisconnectReason _disconnectReason;
+ private ResultCode _nifmResultCode;
+
+ private AccessPoint _accessPoint;
+ private Station _station;
public IUserLocalCommunicationService(ServiceCtx context)
{
- _networkInterface = new NetworkInterface(context.Device.System);
+ _stateChangeEvent = new KEvent(context.Device.System.KernelContext);
+ _state = NetworkState.None;
+ _disconnectReason = DisconnectReason.None;
+ }
+
+ private ushort CheckDevelopmentChannel(ushort channel)
+ {
+ return (ushort)(!IsDevelopment ? 0 : channel);
+ }
+
+ private SecurityMode CheckDevelopmentSecurityMode(SecurityMode securityMode)
+ {
+ return !IsDevelopment ? SecurityMode.Retail : securityMode;
+ }
+
+ private bool CheckLocalCommunicationIdPermission(ServiceCtx context, ulong localCommunicationIdChecked)
+ {
+ // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
+ ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
+
+ foreach (var localCommunicationId in controlProperty.LocalCommunicationId.ItemsRo)
+ {
+ if (localCommunicationId == localCommunicationIdChecked)
+ {
+ return true;
+ }
+ }
+
+ return false;
}
[CommandCmif(0)]
// GetState() -> s32 state
public ResultCode GetState(ServiceCtx context)
{
- if (_networkInterface.NifmState != ResultCode.Success)
+ if (_nifmResultCode != ResultCode.Success)
{
context.ResponseData.Write((int)NetworkState.Error);
return ResultCode.Success;
}
- ResultCode result = _networkInterface.GetState(out NetworkState state);
+ // NOTE: Returns ResultCode.InvalidArgument if _state is null, doesn't occur in our case.
+ context.ResponseData.Write((int)_state);
+
+ return ResultCode.Success;
+ }
+
+ public void SetState()
+ {
+ _stateChangeEvent.WritableEvent.Signal();
+ }
+
+ public void SetState(NetworkState state)
+ {
+ _state = state;
+
+ SetState();
+ }
+
+ [CommandCmif(1)]
+ // GetNetworkInfo() -> buffer<network_info<0x480>, 0x1a>
+ public ResultCode GetNetworkInfo(ServiceCtx context)
+ {
+ ulong bufferPosition = context.Request.RecvListBuff[0].Position;
+
+ MemoryHelper.FillWithZeros(context.Memory, bufferPosition, 0x480);
+
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ ulong infoSize = MemoryHelper.Write(context.Memory, bufferPosition, networkInfo);
- if (result == ResultCode.Success)
+ context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(infoSize);
+
+ return ResultCode.Success;
+ }
+
+ private ResultCode GetNetworkInfoImpl(out NetworkInfo networkInfo)
+ {
+ if (_state == NetworkState.StationConnected)
+ {
+ networkInfo = _station.NetworkInfo;
+ }
+ else if (_state == NetworkState.AccessPointCreated)
{
- context.ResponseData.Write((int)state);
+ networkInfo = _accessPoint.NetworkInfo;
+ }
+ else
+ {
+ networkInfo = new NetworkInfo();
+
+ return ResultCode.InvalidState;
}
- return result;
+ return ResultCode.Success;
+ }
+
+ private NodeLatestUpdate[] GetNodeLatestUpdateImpl(int count)
+ {
+ if (_state == NetworkState.StationConnected)
+ {
+ return _station.LatestUpdates.ConsumeLatestUpdate(count);
+ }
+ else if (_state == NetworkState.AccessPointCreated)
+ {
+ return _accessPoint.LatestUpdates.ConsumeLatestUpdate(count);
+ }
+ else
+ {
+ return Array.Empty<NodeLatestUpdate>();
+ }
+ }
+
+ [CommandCmif(2)]
+ // GetIpv4Address() -> (u32 ip_address, u32 subnet_mask)
+ public ResultCode GetIpv4Address(ServiceCtx context)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ // NOTE: Return ResultCode.InvalidArgument if ip_address and subnet_mask are null, doesn't occur in our case.
+
+ if (_state == NetworkState.AccessPointCreated || _state == NetworkState.StationConnected)
+ {
+ (_, 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
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(3)]
+ // GetDisconnectReason() -> u16 disconnect_reason
+ public ResultCode GetDisconnectReason(ServiceCtx context)
+ {
+ // NOTE: Returns ResultCode.InvalidArgument if _disconnectReason is null, doesn't occur in our case.
+
+ context.ResponseData.Write((short)_disconnectReason);
+
+ return ResultCode.Success;
+ }
+
+ public void SetDisconnectReason(DisconnectReason reason)
+ {
+ if (_state != NetworkState.Initialized)
+ {
+ _disconnectReason = reason;
+
+ SetState(NetworkState.Initialized);
+ }
+ }
+
+ [CommandCmif(4)]
+ // GetSecurityParameter() -> bytes<0x20, 1> security_parameter
+ public ResultCode GetSecurityParameter(ServiceCtx context)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ SecurityParameter securityParameter = new()
+ {
+ Data = new Array16<byte>(),
+ SessionId = networkInfo.NetworkId.SessionId,
+ };
+
+ context.ResponseData.WriteStruct(securityParameter);
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(5)]
+ // GetNetworkConfig() -> bytes<0x20, 8> network_config
+ public ResultCode GetNetworkConfig(ServiceCtx context)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ NetworkConfig networkConfig = new()
+ {
+ IntentId = networkInfo.NetworkId.IntentId,
+ Channel = networkInfo.Common.Channel,
+ NodeCountMax = networkInfo.Ldn.NodeCountMax,
+ LocalCommunicationVersion = networkInfo.Ldn.Nodes[0].LocalCommunicationVersion,
+ Reserved2 = new Array10<byte>(),
+ };
+
+ context.ResponseData.WriteStruct(networkConfig);
+
+ return ResultCode.Success;
}
[CommandCmif(100)]
// AttachStateChangeEvent() -> handle<copy>
public ResultCode AttachStateChangeEvent(ServiceCtx context)
{
- if (_stateChangeEventHandle == 0)
+ if (context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out int stateChangeEventHandle) != Result.Success)
+ {
+ throw new InvalidOperationException("Out of handles!");
+ }
+
+ context.Response.HandleDesc = IpcHandleDesc.MakeCopy(stateChangeEventHandle);
+
+ // Returns ResultCode.InvalidArgument if handle is null, doesn't occur in our case since we already throw an Exception.
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(101)]
+ // GetNetworkInfoLatestUpdate() -> (buffer<network_info<0x480>, 0x1a>, buffer<node_latest_update, 0xa>)
+ public ResultCode GetNetworkInfoLatestUpdate(ServiceCtx context)
+ {
+ ulong bufferPosition = context.Request.RecvListBuff[0].Position;
+
+ MemoryHelper.FillWithZeros(context.Memory, bufferPosition, 0x480);
+
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ ResultCode resultCode = GetNetworkInfoImpl(out NetworkInfo networkInfo);
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ ulong outputPosition = context.Request.RecvListBuff[0].Position;
+ ulong outputSize = context.Request.RecvListBuff[0].Size;
+
+ ulong latestUpdateSize = (ulong)Marshal.SizeOf<NodeLatestUpdate>();
+ int count = (int)(outputSize / latestUpdateSize);
+
+ NodeLatestUpdate[] latestUpdate = GetNodeLatestUpdateImpl(count);
+
+ MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize);
+
+ foreach (NodeLatestUpdate node in latestUpdate)
+ {
+ MemoryHelper.Write(context.Memory, outputPosition, node);
+
+ outputPosition += latestUpdateSize;
+ }
+
+ ulong infoSize = MemoryHelper.Write(context.Memory, bufferPosition, networkInfo);
+
+ context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(infoSize);
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(102)]
+ // Scan(u16 channel, bytes<0x60, 8> scan_filter) -> (u16 count, buffer<network_info, 0x22>)
+ public ResultCode Scan(ServiceCtx context)
+ {
+ return ScanImpl(context);
+ }
+
+ [CommandCmif(103)]
+ // ScanPrivate(u16 channel, bytes<0x60, 8> scan_filter) -> (u16 count, buffer<network_info, 0x22>)
+ public ResultCode ScanPrivate(ServiceCtx context)
+ {
+ return ScanImpl(context, true);
+ }
+
+ private ResultCode ScanImpl(ServiceCtx context, bool isPrivate = false)
+ {
+ ushort channel = (ushort)context.RequestData.ReadUInt64();
+ ScanFilter scanFilter = context.RequestData.ReadStruct<ScanFilter>();
+
+ (ulong bufferPosition, ulong bufferSize) = context.Request.GetBufferType0x22(0);
+
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ if (!isPrivate)
+ {
+ channel = CheckDevelopmentChannel(channel);
+ }
+
+ ResultCode resultCode = ResultCode.InvalidArgument;
+
+ if (bufferSize != 0)
+ {
+ if (bufferPosition != 0)
+ {
+ ScanFilterFlag scanFilterFlag = scanFilter.Flag;
+
+ if (!scanFilterFlag.HasFlag(ScanFilterFlag.NetworkType) || scanFilter.NetworkType <= NetworkType.All)
+ {
+ if (scanFilterFlag.HasFlag(ScanFilterFlag.Ssid))
+ {
+ if (scanFilter.Ssid.Length <= 31)
+ {
+ return resultCode;
+ }
+ }
+
+ if (!scanFilterFlag.HasFlag(ScanFilterFlag.MacAddress))
+ {
+ if (scanFilterFlag > ScanFilterFlag.All)
+ {
+ return resultCode;
+ }
+
+ if (_state - 3 >= NetworkState.AccessPoint)
+ {
+ resultCode = ResultCode.InvalidState;
+ }
+ else
+ {
+ if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1)
+ {
+ // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
+ ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
+
+ scanFilter.NetworkId.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
+ }
+
+ resultCode = ScanInternal(context.Memory, channel, scanFilter, bufferPosition, bufferSize, out ulong counter);
+
+ context.ResponseData.Write(counter);
+ }
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
+ }
+ }
+
+ return resultCode;
+ }
+
+ private ResultCode ScanInternal(IVirtualMemoryManager memory, ushort channel, ScanFilter scanFilter, ulong bufferPosition, ulong bufferSize, out ulong counter)
+ {
+ ulong networkInfoSize = (ulong)Marshal.SizeOf(typeof(NetworkInfo));
+ ulong maxGames = bufferSize / networkInfoSize;
+
+ MemoryHelper.FillWithZeros(memory, bufferPosition, (int)bufferSize);
+
+ NetworkInfo[] availableGames = NetworkClient.Scan(channel, scanFilter);
+
+ counter = 0;
+
+ foreach (NetworkInfo networkInfo in availableGames)
+ {
+ MemoryHelper.Write(memory, bufferPosition + (networkInfoSize * counter), networkInfo);
+
+ if (++counter >= maxGames)
+ {
+ break;
+ }
+ }
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(104)] // 5.0.0+
+ // SetWirelessControllerRestriction(u32 wireless_controller_restriction)
+ public ResultCode SetWirelessControllerRestriction(ServiceCtx context)
+ {
+ // NOTE: Return ResultCode.InvalidArgument if an internal IPAddress is null, doesn't occur in our case.
+
+ uint wirelessControllerRestriction = context.RequestData.ReadUInt32();
+
+ if (wirelessControllerRestriction > 1)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ if (_state != NetworkState.Initialized)
+ {
+ return ResultCode.InvalidState;
+ }
+
+ // NOTE: WirelessControllerRestriction value is used for the btm service in SetWlanMode call.
+ // Since we use our own implementation we can do nothing here.
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(200)]
+ // OpenAccessPoint()
+ public ResultCode OpenAccessPoint(ServiceCtx context)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ if (_state != NetworkState.Initialized)
+ {
+ return ResultCode.InvalidState;
+ }
+
+ CloseStation();
+
+ SetState(NetworkState.AccessPoint);
+
+ _accessPoint = new AccessPoint(this);
+
+ // NOTE: Calls nifm service and return related result codes.
+ // Since we use our own implementation we can return ResultCode.Success.
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(201)]
+ // CloseAccessPoint()
+ public ResultCode CloseAccessPoint(ServiceCtx context)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ if (_state == NetworkState.AccessPoint || _state == NetworkState.AccessPointCreated)
+ {
+ DestroyNetworkImpl(DisconnectReason.DestroyedByUser);
+ }
+ else
+ {
+ return ResultCode.InvalidState;
+ }
+
+ SetState(NetworkState.Initialized);
+
+ return ResultCode.Success;
+ }
+
+ private void CloseAccessPoint()
+ {
+ _accessPoint?.Dispose();
+ _accessPoint = null;
+ }
+
+ [CommandCmif(202)]
+ // CreateNetwork(bytes<0x44, 2> security_config, bytes<0x30, 1> user_config, bytes<0x20, 8> network_config)
+ public ResultCode CreateNetwork(ServiceCtx context)
+ {
+ return CreateNetworkImpl(context);
+ }
+
+ [CommandCmif(203)]
+ // CreateNetworkPrivate(bytes<0x44, 2> security_config, bytes<0x20, 1> security_parameter, bytes<0x30, 1>, bytes<0x20, 8> network_config, buffer<unknown, 9> address_entry, int count)
+ public ResultCode CreateNetworkPrivate(ServiceCtx context)
+ {
+ return CreateNetworkImpl(context, true);
+ }
+
+ public ResultCode CreateNetworkImpl(ServiceCtx context, bool isPrivate = false)
+ {
+ SecurityConfig securityConfig = context.RequestData.ReadStruct<SecurityConfig>();
+ SecurityParameter securityParameter = isPrivate ? context.RequestData.ReadStruct<SecurityParameter>() : new SecurityParameter();
+
+ UserConfig userConfig = context.RequestData.ReadStruct<UserConfig>();
+
+ context.RequestData.BaseStream.Seek(4, SeekOrigin.Current); // Alignment?
+ NetworkConfig networkConfig = context.RequestData.ReadStruct<NetworkConfig>();
+
+ if (networkConfig.IntentId.LocalCommunicationId == -1)
+ {
+ // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
+ ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
+
+ networkConfig.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
+ }
+
+ bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkConfig.IntentId.LocalCommunicationId);
+ if (!isLocalCommunicationIdValid)
+ {
+ return ResultCode.InvalidObject;
+ }
+
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ networkConfig.Channel = CheckDevelopmentChannel(networkConfig.Channel);
+ securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
+
+ if (networkConfig.NodeCountMax <= 8)
+ {
+ if ((((ulong)networkConfig.LocalCommunicationVersion) & 0x80000000) == 0)
+ {
+ if (securityConfig.SecurityMode <= SecurityMode.Retail)
+ {
+ if (securityConfig.Passphrase.Length <= 0x40)
+ {
+ if (_state == NetworkState.AccessPoint)
+ {
+ if (isPrivate)
+ {
+ ulong bufferPosition = context.Request.PtrBuff[0].Position;
+ ulong bufferSize = context.Request.PtrBuff[0].Size;
+
+ byte[] addressListBytes = new byte[bufferSize];
+
+ context.Memory.Read(bufferPosition, addressListBytes);
+
+ AddressList addressList = MemoryMarshal.Cast<byte, AddressList>(addressListBytes)[0];
+
+ _accessPoint.CreateNetworkPrivate(securityConfig, securityParameter, userConfig, networkConfig, addressList);
+ }
+ else
+ {
+ _accessPoint.CreateNetwork(securityConfig, userConfig, networkConfig);
+ }
+
+ return ResultCode.Success;
+ }
+ else
+ {
+ return ResultCode.InvalidState;
+ }
+ }
+ }
+ }
+ }
+
+ return ResultCode.InvalidArgument;
+ }
+
+ [CommandCmif(204)]
+ // DestroyNetwork()
+ public ResultCode DestroyNetwork(ServiceCtx context)
+ {
+ return DestroyNetworkImpl(DisconnectReason.DestroyedByUser);
+ }
+
+ private ResultCode DestroyNetworkImpl(DisconnectReason disconnectReason)
+ {
+ if (_nifmResultCode != ResultCode.Success)
{
- if (context.Process.HandleTable.GenerateHandle(_networkInterface.StateChangeEvent.ReadableEvent, out _stateChangeEventHandle) != Result.Success)
+ return _nifmResultCode;
+ }
+
+ if (disconnectReason - 3 <= DisconnectReason.DisconnectedByUser)
+ {
+ if (_state == NetworkState.AccessPointCreated)
{
- throw new InvalidOperationException("Out of handles!");
+ CloseAccessPoint();
+
+ SetState(NetworkState.AccessPoint);
+
+ return ResultCode.Success;
}
+
+ CloseAccessPoint();
+
+ return ResultCode.InvalidState;
+ }
+
+ return ResultCode.InvalidArgument;
+ }
+
+ [CommandCmif(205)]
+ // Reject(u32 node_id)
+ public ResultCode Reject(ServiceCtx context)
+ {
+ uint nodeId = context.RequestData.ReadUInt32();
+
+ return RejectImpl(DisconnectReason.Rejected, nodeId);
+ }
+
+ private ResultCode RejectImpl(DisconnectReason disconnectReason, uint nodeId)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ if (_state != NetworkState.AccessPointCreated)
+ {
+ return ResultCode.InvalidState; // Must be network host to reject nodes.
+ }
+
+ return NetworkClient.Reject(disconnectReason, nodeId);
+ }
+
+ [CommandCmif(206)]
+ // SetAdvertiseData(buffer<advertise_data, 0x21>)
+ public ResultCode SetAdvertiseData(ServiceCtx context)
+ {
+ (ulong bufferPosition, ulong bufferSize) = context.Request.GetBufferType0x21(0);
+
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ if (bufferSize == 0 || bufferSize > 0x180)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ if (_state == NetworkState.AccessPoint || _state == NetworkState.AccessPointCreated)
+ {
+ byte[] advertiseData = new byte[bufferSize];
+
+ context.Memory.Read(bufferPosition, advertiseData);
+
+ return _accessPoint.SetAdvertiseData(advertiseData);
+ }
+ else
+ {
+ return ResultCode.InvalidState;
+ }
+ }
+
+ [CommandCmif(207)]
+ // SetStationAcceptPolicy(u8 accept_policy)
+ public ResultCode SetStationAcceptPolicy(ServiceCtx context)
+ {
+ AcceptPolicy acceptPolicy = (AcceptPolicy)context.RequestData.ReadByte();
+
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ if (acceptPolicy > AcceptPolicy.WhiteList)
+ {
+ return ResultCode.InvalidArgument;
+ }
+
+ if (_state == NetworkState.AccessPoint || _state == NetworkState.AccessPointCreated)
+ {
+ return _accessPoint.SetStationAcceptPolicy(acceptPolicy);
+ }
+ else
+ {
+ return ResultCode.InvalidState;
+ }
+ }
+
+ [CommandCmif(208)]
+ // AddAcceptFilterEntry(bytes<6, 1> mac_address)
+ public ResultCode AddAcceptFilterEntry(ServiceCtx context)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ // TODO
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(209)]
+ // ClearAcceptFilter()
+ public ResultCode ClearAcceptFilter(ServiceCtx context)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ // TODO
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(300)]
+ // OpenStation()
+ public ResultCode OpenStation(ServiceCtx context)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ if (_state != NetworkState.Initialized)
+ {
+ return ResultCode.InvalidState;
+ }
+
+ CloseAccessPoint();
+
+ SetState(NetworkState.Station);
+
+ _station?.Dispose();
+ _station = new Station(this);
+
+ // NOTE: Calls nifm service and returns related result codes.
+ // Since we use our own implementation we can return ResultCode.Success.
+
+ return ResultCode.Success;
+ }
+
+ [CommandCmif(301)]
+ // CloseStation()
+ public ResultCode CloseStation(ServiceCtx context)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
}
- context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_stateChangeEventHandle);
+ if (_state == NetworkState.Station || _state == NetworkState.StationConnected)
+ {
+ DisconnectImpl(DisconnectReason.DisconnectedByUser);
+ }
+ else
+ {
+ return ResultCode.InvalidState;
+ }
- // Return ResultCode.InvalidArgument if handle is null, doesn't occur in our case since we already throw an Exception.
+ SetState(NetworkState.Initialized);
return ResultCode.Success;
}
+ private void CloseStation()
+ {
+ _station?.Dispose();
+ _station = null;
+ }
+
+ [CommandCmif(302)]
+ // Connect(bytes<0x44, 2> security_config, bytes<0x30, 1> user_config, u32 local_communication_version, u32 option_unknown, buffer<network_info<0x480>, 0x19>)
+ public ResultCode Connect(ServiceCtx context)
+ {
+ return ConnectImpl(context);
+ }
+
+ [CommandCmif(303)]
+ // ConnectPrivate(bytes<0x44, 2> security_config, bytes<0x20, 1> security_parameter, bytes<0x30, 1> user_config, u32 local_communication_version, u32 option_unknown, bytes<0x20, 8> network_config)
+ public ResultCode ConnectPrivate(ServiceCtx context)
+ {
+ return ConnectImpl(context, true);
+ }
+
+ private ResultCode ConnectImpl(ServiceCtx context, bool isPrivate = false)
+ {
+ SecurityConfig securityConfig = context.RequestData.ReadStruct<SecurityConfig>();
+ SecurityParameter securityParameter = isPrivate ? context.RequestData.ReadStruct<SecurityParameter>() : new SecurityParameter();
+
+ UserConfig userConfig = context.RequestData.ReadStruct<UserConfig>();
+ uint localCommunicationVersion = context.RequestData.ReadUInt32();
+ uint optionUnknown = context.RequestData.ReadUInt32();
+
+ NetworkConfig networkConfig = new();
+ NetworkInfo networkInfo = new();
+
+ if (isPrivate)
+ {
+ context.RequestData.ReadUInt32(); // Padding.
+
+ networkConfig = context.RequestData.ReadStruct<NetworkConfig>();
+ }
+ else
+ {
+ ulong bufferPosition = context.Request.PtrBuff[0].Position;
+ ulong bufferSize = context.Request.PtrBuff[0].Size;
+
+ byte[] networkInfoBytes = new byte[bufferSize];
+
+ context.Memory.Read(bufferPosition, networkInfoBytes);
+
+ networkInfo = MemoryMarshal.Cast<byte, NetworkInfo>(networkInfoBytes)[0];
+ }
+
+ if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1)
+ {
+ // TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
+ ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
+
+ networkInfo.NetworkId.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
+ }
+
+ bool isLocalCommunicationIdValid = CheckLocalCommunicationIdPermission(context, (ulong)networkInfo.NetworkId.IntentId.LocalCommunicationId);
+ if (!isLocalCommunicationIdValid)
+ {
+ return ResultCode.InvalidObject;
+ }
+
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ securityConfig.SecurityMode = CheckDevelopmentSecurityMode(securityConfig.SecurityMode);
+
+ ResultCode resultCode = ResultCode.InvalidArgument;
+
+ if (securityConfig.SecurityMode - 1 <= SecurityMode.Debug)
+ {
+ if (optionUnknown <= 1 && (localCommunicationVersion >> 15) == 0 && securityConfig.PassphraseSize <= 64)
+ {
+ resultCode = ResultCode.VersionTooLow;
+ if (localCommunicationVersion >= 0)
+ {
+ resultCode = ResultCode.VersionTooHigh;
+ if (localCommunicationVersion <= short.MaxValue)
+ {
+ if (_state != NetworkState.Station)
+ {
+ resultCode = ResultCode.InvalidState;
+ }
+ else
+ {
+ if (isPrivate)
+ {
+ resultCode = _station.ConnectPrivate(securityConfig, securityParameter, userConfig, localCommunicationVersion, optionUnknown, networkConfig);
+ }
+ else
+ {
+ resultCode = _station.Connect(securityConfig, userConfig, localCommunicationVersion, optionUnknown, networkInfo);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return resultCode;
+ }
+
+ [CommandCmif(304)]
+ // Disconnect()
+ public ResultCode Disconnect(ServiceCtx context)
+ {
+ return DisconnectImpl(DisconnectReason.DisconnectedByUser);
+ }
+
+ private ResultCode DisconnectImpl(DisconnectReason disconnectReason)
+ {
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ if (disconnectReason <= DisconnectReason.DisconnectedBySystem)
+ {
+ if (_state == NetworkState.StationConnected)
+ {
+ SetState(NetworkState.Station);
+
+ CloseStation();
+
+ _disconnectReason = disconnectReason;
+
+ return ResultCode.Success;
+ }
+
+ CloseStation();
+
+ return ResultCode.InvalidState;
+ }
+
+ return ResultCode.InvalidArgument;
+ }
+
[CommandCmif(400)]
- // InitializeOld(u64, pid)
+ // InitializeOld(pid)
public ResultCode InitializeOld(ServiceCtx context)
{
- return _networkInterface.Initialize(UnknownValue, 0, null, null);
+ return InitializeImpl(context, context.Process.Pid, NifmRequestID);
}
[CommandCmif(401)]
// Finalize()
public ResultCode Finalize(ServiceCtx context)
{
- return _networkInterface.Finalize();
+ if (_nifmResultCode != ResultCode.Success)
+ {
+ return _nifmResultCode;
+ }
+
+ // NOTE: Use true when its called in nn::ldn::detail::ISystemLocalCommunicationService
+ ResultCode resultCode = FinalizeImpl(false);
+ if (resultCode == ResultCode.Success)
+ {
+ SetDisconnectReason(DisconnectReason.None);
+ }
+
+ return resultCode;
+ }
+
+ private ResultCode FinalizeImpl(bool isCausedBySystem)
+ {
+ DisconnectReason disconnectReason;
+
+ switch (_state)
+ {
+ case NetworkState.None:
+ return ResultCode.Success;
+ case NetworkState.AccessPoint:
+ {
+ CloseAccessPoint();
+
+ break;
+ }
+ case NetworkState.AccessPointCreated:
+ {
+ if (isCausedBySystem)
+ {
+ disconnectReason = DisconnectReason.DestroyedBySystem;
+ }
+ else
+ {
+ disconnectReason = DisconnectReason.DestroyedByUser;
+ }
+
+ DestroyNetworkImpl(disconnectReason);
+
+ break;
+ }
+ case NetworkState.Station:
+ {
+ CloseStation();
+
+ break;
+ }
+ case NetworkState.StationConnected:
+ {
+ if (isCausedBySystem)
+ {
+ disconnectReason = DisconnectReason.DisconnectedBySystem;
+ }
+ else
+ {
+ disconnectReason = DisconnectReason.DisconnectedByUser;
+ }
+
+ DisconnectImpl(disconnectReason);
+
+ break;
+ }
+ }
+
+ SetState(NetworkState.None);
+
+ NetworkClient?.DisconnectAndStop();
+ NetworkClient = null;
+
+ return ResultCode.Success;
}
[CommandCmif(402)] // 7.0.0+
- // Initialize(u64 ip_addresses, u64, pid)
+ // Initialize(u64 ip_addresses, pid)
public ResultCode Initialize(ServiceCtx context)
{
- // TODO(Ac_K): Determine what addresses are.
- IPAddress unknownAddress1 = new(context.RequestData.ReadUInt32());
- IPAddress unknownAddress2 = new(context.RequestData.ReadUInt32());
+ _ = new IPAddress(context.RequestData.ReadUInt32());
+ _ = new IPAddress(context.RequestData.ReadUInt32());
+
+ // NOTE: It seems the guest can get ip_address and subnet_mask from nifm service and pass it through the initialize.
+ // This calls InitializeImpl() twice: The first time with NIFM_REQUEST_ID, and if it fails, a second time with nifm_request_id = 1.
+
+ return InitializeImpl(context, context.Process.Pid, NifmRequestID);
+ }
+
+ public ResultCode InitializeImpl(ServiceCtx context, ulong pid, int nifmRequestId)
+ {
+ ResultCode resultCode = ResultCode.InvalidArgument;
+
+ if (nifmRequestId <= 255)
+ {
+ if (_state != NetworkState.Initialized)
+ {
+ // NOTE: Service calls nn::ldn::detail::NetworkInterfaceManager::NetworkInterfaceMonitor::Initialize() with nifmRequestId as argument,
+ // then it stores the result code of it in a global variable. Since we use our own implementation, we can just check the connection
+ // and return related error codes.
+ if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable())
+ {
+ MultiplayerMode mode = context.Device.Configuration.MultiplayerMode;
+ switch (mode)
+ {
+ case MultiplayerMode.Disabled:
+ NetworkClient = new DisabledLdnClient();
+ break;
+ }
+
+ // TODO: Call nn::arp::GetApplicationLaunchProperty here when implemented.
+ NetworkClient.SetGameVersion(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion.Items.ToArray());
+
+ resultCode = ResultCode.Success;
+
+ _nifmResultCode = resultCode;
+
+ SetState(NetworkState.Initialized);
+ }
+ else
+ {
+ // NOTE: Service returns differents ResultCode here related to the nifm ResultCode.
+ resultCode = ResultCode.DeviceDisabled;
+ _nifmResultCode = resultCode;
+ }
+ }
+ }
+
+ return resultCode;
+ }
+
+ public void Dispose()
+ {
+ if (NetworkClient != null)
+ {
+ _station?.Dispose();
+ _accessPoint?.Dispose();
+
+ NetworkClient.DisconnectAndStop();
+ }
- return _networkInterface.Initialize(UnknownValue, version: 1, unknownAddress1, unknownAddress2);
+ NetworkClient = null;
}
}
}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
new file mode 100644
index 00000000..9ff46ccc
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/ConnectRequest.cs
@@ -0,0 +1,15 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x4FC)]
+ struct ConnectRequest
+ {
+ public SecurityConfig SecurityConfig;
+ public UserConfig UserConfig;
+ public uint LocalCommunicationVersion;
+ public uint OptionUnknown;
+ public NetworkInfo NetworkInfo;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
new file mode 100644
index 00000000..4efe9165
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Network/Types/CreateAccessPointRequest.cs
@@ -0,0 +1,16 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Network.Types
+{
+ /// <remarks>
+ /// Advertise data is appended separately (remaining data in the buffer).
+ /// </remarks>
+ [StructLayout(LayoutKind.Sequential, Size = 0x94, CharSet = CharSet.Ansi)]
+ struct CreateAccessPointRequest
+ {
+ public SecurityConfig SecurityConfig;
+ public UserConfig UserConfig;
+ public NetworkConfig NetworkConfig;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
new file mode 100644
index 00000000..75a1e35f
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/DisabledLdnClient.cs
@@ -0,0 +1,62 @@
+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 System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+{
+ class DisabledLdnClient : INetworkClient
+ {
+ public event EventHandler<NetworkChangeEventArgs> NetworkChange;
+
+ public NetworkError Connect(ConnectRequest request)
+ {
+ NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
+
+ return NetworkError.None;
+ }
+
+ public NetworkError ConnectPrivate(ConnectPrivateRequest request)
+ {
+ NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
+
+ return NetworkError.None;
+ }
+
+ public bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData)
+ {
+ NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
+
+ return true;
+ }
+
+ public bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData)
+ {
+ NetworkChange?.Invoke(this, new NetworkChangeEventArgs(new NetworkInfo(), false));
+
+ return true;
+ }
+
+ public void DisconnectAndStop() { }
+
+ public void DisconnectNetwork() { }
+
+ public ResultCode Reject(DisconnectReason disconnectReason, uint nodeId)
+ {
+ return ResultCode.Success;
+ }
+
+ public NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter)
+ {
+ return Array.Empty<NetworkInfo>();
+ }
+
+ public void SetAdvertiseData(byte[] data) { }
+
+ public void SetGameVersion(byte[] versionString) { }
+
+ public void SetStationAcceptPolicy(AcceptPolicy acceptPolicy) { }
+
+ public void Dispose() { }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
new file mode 100644
index 00000000..ff342d27
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/INetworkClient.cs
@@ -0,0 +1,24 @@
+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 System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+{
+ interface INetworkClient : IDisposable
+ {
+ event EventHandler<NetworkChangeEventArgs> NetworkChange;
+
+ void DisconnectNetwork();
+ void DisconnectAndStop();
+ NetworkError Connect(ConnectRequest request);
+ NetworkError ConnectPrivate(ConnectPrivateRequest request);
+ ResultCode Reject(DisconnectReason disconnectReason, uint nodeId);
+ NetworkInfo[] Scan(ushort channel, ScanFilter scanFilter);
+ void SetGameVersion(byte[] versionString);
+ void SetStationAcceptPolicy(AcceptPolicy acceptPolicy);
+ void SetAdvertiseData(byte[] data);
+ bool CreateNetwork(CreateAccessPointRequest request, byte[] advertiseData);
+ bool CreateNetworkPrivate(CreateAccessPointPrivateRequest request, byte[] advertiseData);
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
new file mode 100644
index 00000000..1cc09c00
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/NetworkChangeEventArgs.cs
@@ -0,0 +1,24 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn
+{
+ class NetworkChangeEventArgs : EventArgs
+ {
+ public NetworkInfo Info;
+ public bool Connected;
+ public DisconnectReason DisconnectReason;
+
+ public NetworkChangeEventArgs(NetworkInfo info, bool connected, DisconnectReason disconnectReason = DisconnectReason.None)
+ {
+ Info = info;
+ Connected = connected;
+ DisconnectReason = disconnectReason;
+ }
+
+ public DisconnectReason DisconnectReasonOrDefault(DisconnectReason defaultReason)
+ {
+ return DisconnectReason == DisconnectReason.None ? defaultReason : DisconnectReason;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
new file mode 100644
index 00000000..47e48d0a
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/ConnectPrivateRequest.cs
@@ -0,0 +1,16 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0xBC)]
+ struct ConnectPrivateRequest
+ {
+ public SecurityConfig SecurityConfig;
+ public SecurityParameter SecurityParameter;
+ public UserConfig UserConfig;
+ public uint LocalCommunicationVersion;
+ public uint OptionUnknown;
+ public NetworkConfig NetworkConfig;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
new file mode 100644
index 00000000..6e890618
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/CreateAccessPointPrivateRequest.cs
@@ -0,0 +1,18 @@
+using Ryujinx.HLE.HOS.Services.Ldn.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+{
+ /// <remarks>
+ /// Advertise data is appended separately (remaining data in the buffer).
+ /// </remarks>
+ [StructLayout(LayoutKind.Sequential, Size = 0x13C, Pack = 1)]
+ struct CreateAccessPointPrivateRequest
+ {
+ public SecurityConfig SecurityConfig;
+ public SecurityParameter SecurityParameter;
+ public UserConfig UserConfig;
+ public NetworkConfig NetworkConfig;
+ public AddressList AddressList;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
new file mode 100644
index 00000000..70ebf7e3
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkError.cs
@@ -0,0 +1,22 @@
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+{
+ enum NetworkError : int
+ {
+ None,
+
+ PortUnreachable,
+
+ TooManyPlayers,
+ VersionTooLow,
+ VersionTooHigh,
+
+ ConnectFailure,
+ ConnectNotFound,
+ ConnectTimeout,
+ ConnectRejected,
+
+ RejectFailed,
+
+ Unknown = -1,
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
new file mode 100644
index 00000000..acb0b36a
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/RyuLdn/Types/NetworkErrorMessage.cs
@@ -0,0 +1,10 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.RyuLdn.Types
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x4)]
+ struct NetworkErrorMessage
+ {
+ public NetworkError Error;
+ }
+}
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
new file mode 100644
index 00000000..c190d6ed
--- /dev/null
+++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/Station.cs
@@ -0,0 +1,115 @@
+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 System;
+
+namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
+{
+ class Station : IDisposable
+ {
+ public NetworkInfo NetworkInfo;
+ public Array8<NodeLatestUpdate> LatestUpdates = new();
+
+ private readonly IUserLocalCommunicationService _parent;
+
+ public bool Connected { get; private set; }
+
+ public Station(IUserLocalCommunicationService parent)
+ {
+ _parent = parent;
+
+ _parent.NetworkClient.NetworkChange += NetworkChanged;
+ }
+
+ private void NetworkChanged(object sender, RyuLdn.NetworkChangeEventArgs e)
+ {
+ LatestUpdates.CalculateLatestUpdate(NetworkInfo.Ldn.Nodes, e.Info.Ldn.Nodes);
+
+ NetworkInfo = e.Info;
+
+ if (Connected != e.Connected)
+ {
+ Connected = e.Connected;
+
+ if (Connected)
+ {
+ _parent.SetState(NetworkState.StationConnected);
+ }
+ else
+ {
+ _parent.SetDisconnectReason(e.DisconnectReasonOrDefault(DisconnectReason.DestroyedByUser));
+ }
+ }
+ else
+ {
+ _parent.SetState();
+ }
+ }
+
+ public void Dispose()
+ {
+ _parent.NetworkClient.DisconnectNetwork();
+
+ _parent.NetworkClient.NetworkChange -= NetworkChanged;
+ }
+
+ private ResultCode NetworkErrorToResult(NetworkError error)
+ {
+ return error switch
+ {
+ NetworkError.None => ResultCode.Success,
+ NetworkError.VersionTooLow => ResultCode.VersionTooLow,
+ NetworkError.VersionTooHigh => ResultCode.VersionTooHigh,
+ NetworkError.TooManyPlayers => ResultCode.TooManyPlayers,
+
+ NetworkError.ConnectFailure => ResultCode.ConnectFailure,
+ NetworkError.ConnectNotFound => ResultCode.ConnectNotFound,
+ NetworkError.ConnectTimeout => ResultCode.ConnectTimeout,
+ NetworkError.ConnectRejected => ResultCode.ConnectRejected,
+
+ _ => ResultCode.DeviceNotAvailable,
+ };
+ }
+
+ public ResultCode Connect(
+ SecurityConfig securityConfig,
+ UserConfig userConfig,
+ uint localCommunicationVersion,
+ uint optionUnknown,
+ NetworkInfo networkInfo)
+ {
+ ConnectRequest request = new()
+ {
+ SecurityConfig = securityConfig,
+ UserConfig = userConfig,
+ LocalCommunicationVersion = localCommunicationVersion,
+ OptionUnknown = optionUnknown,
+ NetworkInfo = networkInfo,
+ };
+
+ return NetworkErrorToResult(_parent.NetworkClient.Connect(request));
+ }
+
+ public ResultCode ConnectPrivate(
+ SecurityConfig securityConfig,
+ SecurityParameter securityParameter,
+ UserConfig userConfig,
+ uint localCommunicationVersion,
+ uint optionUnknown,
+ NetworkConfig networkConfig)
+ {
+ ConnectPrivateRequest request = new()
+ {
+ SecurityConfig = securityConfig,
+ SecurityParameter = securityParameter,
+ UserConfig = userConfig,
+ LocalCommunicationVersion = localCommunicationVersion,
+ OptionUnknown = optionUnknown,
+ NetworkConfig = networkConfig,
+ };
+
+ return NetworkErrorToResult(_parent.NetworkClient.ConnectPrivate(request));
+ }
+ }
+}