diff options
Diffstat (limited to 'src/Ryujinx.HLE/HOS/Services/Ldn')
35 files changed, 1804 insertions, 27 deletions
diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/IUserServiceCreator.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/IUserServiceCreator.cs index 317b1dbe..7bcb7785 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/IUserServiceCreator.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/IUserServiceCreator.cs @@ -5,7 +5,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn [Service("ldn:u")] class IUserServiceCreator : IpcService { - public IUserServiceCreator(ServiceCtx context) { } + public IUserServiceCreator(ServiceCtx context) : base(context.Device.System.LdnServer) { } [CommandCmif(0)] // CreateUserLocalCommunicationService() -> object<nn::ldn::detail::IUserLocalCommunicationService> diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/ResultCode.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/ResultCode.cs index a9f2cbc3..d550ea57 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/ResultCode.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/ResultCode.cs @@ -7,10 +7,18 @@ namespace Ryujinx.HLE.HOS.Services.Ldn Success = 0, + DeviceNotAvailable = (16 << ErrorCodeShift) | ModuleId, DeviceDisabled = (22 << ErrorCodeShift) | ModuleId, InvalidState = (32 << ErrorCodeShift) | ModuleId, - Unknown1 = (48 << ErrorCodeShift) | ModuleId, + NodeNotFound = (48 << ErrorCodeShift) | ModuleId, + ConnectFailure = (64 << ErrorCodeShift) | ModuleId, + ConnectNotFound = (65 << ErrorCodeShift) | ModuleId, + ConnectTimeout = (66 << ErrorCodeShift) | ModuleId, + ConnectRejected = (67 << ErrorCodeShift) | ModuleId, InvalidArgument = (96 << ErrorCodeShift) | ModuleId, InvalidObject = (97 << ErrorCodeShift) | ModuleId, + VersionTooLow = (113 << ErrorCodeShift) | ModuleId, + VersionTooHigh = (114 << ErrorCodeShift) | ModuleId, + TooManyPlayers = (144 << ErrorCodeShift) | ModuleId, } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AcceptPolicy.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AcceptPolicy.cs new file mode 100644 index 00000000..272a2fd8 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AcceptPolicy.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + enum AcceptPolicy : byte + { + AcceptAll, + RejectAll, + BlackList, + WhiteList, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressEntry.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressEntry.cs new file mode 100644 index 00000000..a458c521 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressEntry.cs @@ -0,0 +1,13 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0xC)] + struct AddressEntry + { + public uint Ipv4Address; + public Array6<byte> MacAddress; + public ushort Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressList.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressList.cs new file mode 100644 index 00000000..cf4d0e20 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/AddressList.cs @@ -0,0 +1,11 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x60)] + struct AddressList + { + public Array8<AddressEntry> Addresses; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/CommonNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/CommonNetworkInfo.cs new file mode 100644 index 00000000..cd3a5716 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/CommonNetworkInfo.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x30)] + struct CommonNetworkInfo + { + public Array6<byte> MacAddress; + public Ssid Ssid; + public ushort Channel; + public byte LinkLevel; + public byte NetworkType; + public uint Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/DisconnectReason.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/DisconnectReason.cs new file mode 100644 index 00000000..e3fd0ed4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/DisconnectReason.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + enum DisconnectReason : uint + { + None, + DisconnectedByUser, + DisconnectedBySystem, + DestroyedByUser, + DestroyedBySystem, + Rejected, + SignalLost, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/IntentId.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/IntentId.cs new file mode 100644 index 00000000..e1ffec5e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/IntentId.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct IntentId + { + public long LocalCommunicationId; + public ushort Reserved1; + public ushort SceneId; + public uint Reserved2; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs new file mode 100644 index 00000000..4b7241c4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/LdnNetworkInfo.cs @@ -0,0 +1,23 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x430)] + struct LdnNetworkInfo + { + public Array16<byte> SecurityParameter; + public ushort SecurityMode; + public AcceptPolicy StationAcceptPolicy; + public byte Reserved1; + public ushort Reserved2; + public byte NodeCountMax; + public byte NodeCount; + public Array8<NodeInfo> Nodes; + public ushort Reserved3; + public ushort AdvertiseDataSize; + public Array384<byte> AdvertiseData; + public Array140<byte> Reserved4; + public ulong AuthenticationId; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs new file mode 100644 index 00000000..aea9a4a7 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkConfig.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x20)] + struct NetworkConfig + { + public IntentId IntentId; + public ushort Channel; + public byte NodeCountMax; + public byte Reserved1; + public ushort LocalCommunicationVersion; + public Array10<byte> Reserved2; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkId.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkId.cs new file mode 100644 index 00000000..9579647b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkId.cs @@ -0,0 +1,12 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x20)] + struct NetworkId + { + public IntentId IntentId; + public Array16<byte> SessionId; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkInfo.cs new file mode 100644 index 00000000..c1fb87d4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkInfo.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x480)] + struct NetworkInfo + { + public NetworkId NetworkId; + public CommonNetworkInfo Common; + public LdnNetworkInfo Ldn; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkType.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkType.cs new file mode 100644 index 00000000..a9ca153f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NetworkType.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + enum NetworkType : uint + { + None, + General, + Ldn, + All, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs new file mode 100644 index 00000000..c57a7dc4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeInfo.cs @@ -0,0 +1,18 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x40)] + struct NodeInfo + { + public uint Ipv4Address; + public Array6<byte> MacAddress; + public byte NodeId; + public byte IsConnected; + public Array33<byte> UserName; + public byte Reserved1; + public ushort LocalCommunicationVersion; + public Array16<byte> Reserved2; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs new file mode 100644 index 00000000..f33ceaeb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdate.cs @@ -0,0 +1,62 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 8)] + struct NodeLatestUpdate + { + public NodeLatestUpdateFlags State; + public Array7<byte> Reserved; + } + + static class NodeLatestUpdateHelper + { + private static readonly object _lock = new(); + + public static void CalculateLatestUpdate(this Array8<NodeLatestUpdate> array, Array8<NodeInfo> beforeNodes, Array8<NodeInfo> afterNodes) + { + lock (_lock) + { + for (int i = 0; i < 8; i++) + { + if (beforeNodes[i].IsConnected == 0) + { + if (afterNodes[i].IsConnected != 0) + { + array[i].State |= NodeLatestUpdateFlags.Connect; + } + } + else + { + if (afterNodes[i].IsConnected == 0) + { + array[i].State |= NodeLatestUpdateFlags.Disconnect; + } + } + } + } + } + + public static NodeLatestUpdate[] ConsumeLatestUpdate(this Array8<NodeLatestUpdate> array, int number) + { + NodeLatestUpdate[] result = new NodeLatestUpdate[number]; + + lock (_lock) + { + for (int i = 0; i < number; i++) + { + result[i].Reserved = new Array7<byte>(); + + if (i < 8) + { + result[i].State = array[i].State; + array[i].State = NodeLatestUpdateFlags.None; + } + } + } + + return result; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdateFlags.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdateFlags.cs new file mode 100644 index 00000000..3b69b279 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/NodeLatestUpdateFlags.cs @@ -0,0 +1,12 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [Flags] + enum NodeLatestUpdateFlags : byte + { + None = 0, + Connect = 1 << 0, + Disconnect = 1 << 1, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs new file mode 100644 index 00000000..a5991074 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilter.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x60)] + struct ScanFilter + { + public NetworkId NetworkId; + public NetworkType NetworkType; + public Array6<byte> MacAddress; + public Ssid Ssid; + public Array16<byte> Reserved; + public ScanFilterFlag Flag; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilterFlag.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilterFlag.cs new file mode 100644 index 00000000..f27b52c3 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/ScanFilterFlag.cs @@ -0,0 +1,18 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [Flags] + enum ScanFilterFlag : byte + { + LocalCommunicationId = 1 << 0, + SessionId = 1 << 1, + NetworkType = 1 << 2, + MacAddress = 1 << 3, + Ssid = 1 << 4, + SceneId = 1 << 5, + IntentId = LocalCommunicationId | SceneId, + NetworkId = IntentId | SessionId, + All = NetworkType | IntentId | SessionId | MacAddress | Ssid, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs new file mode 100644 index 00000000..85a19a87 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityConfig.cs @@ -0,0 +1,13 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x44)] + struct SecurityConfig + { + public SecurityMode SecurityMode; + public ushort PassphraseSize; + public Array64<byte> Passphrase; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityMode.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityMode.cs new file mode 100644 index 00000000..a621d20f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + enum SecurityMode : ushort + { + All, + Retail, + Debug, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs new file mode 100644 index 00000000..534dbc7a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/SecurityParameter.cs @@ -0,0 +1,12 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x20)] + struct SecurityParameter + { + public Array16<byte> Data; + public Array16<byte> SessionId; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs new file mode 100644 index 00000000..72db4d41 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/Ssid.cs @@ -0,0 +1,12 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x22)] + struct Ssid + { + public byte Length; + public Array33<byte> Name; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs new file mode 100644 index 00000000..1401f521 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Types/UserConfig.cs @@ -0,0 +1,12 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Ldn.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x30)] + struct UserConfig + { + public Array33<byte> UserName; + public Array15<byte> Unknown1; + } +} 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)); + } + } +} |
