From 333651d346db30d6db0f157150d12209ca9120f9 Mon Sep 17 00:00:00 2001
From: Ac_K <Acoustik666@gmail.com>
Date: Fri, 6 Sep 2019 16:58:50 +0200
Subject: [PATCH] Implement Bluetooth, Btm, Hid and Nsd services and calls.
 (#761)

- Implement `btdrv` service (IBluetoothDriver).
  Implement call `InitializeBluetoothLe` for initialize events of `bt` service according to RE.

- Implement `bt` service (IBluetoothUser).
  Implement call `RegisterBleEvent` according to RE.

- Add a placeholder for the `btm` service (close #750).

- Implement `btm:u` service (IBtmUser) (close #751).
  Implement call `GetCore` according to RE (close #752).

- Implement `IBtmUserCore` and calls `AcquireBleScanEvent`, `AcquireBleConnectionEvent`, `AcquireBleServiceDiscoveryEvent` and `AcquireBleMtuConfigEvent` according to RE.

- Implement `SetPalmaBoostMode` in `IHidServer` according to RE.

- Add stub for `SetIsPalmaAllConnectable` in `IHidServer` because we will not support Palma devices soon.

- Implement `nsd:a` and `nsd:u` service (IManager) (close #755).
  Implement call `ResolveEx` according to RE (close #756).
  Implement calls `GetSettingName`, `GetEnvironmentIdentifier`, `GetDeviceId`, `DeleteSettings`, `Resolve`, `ReadSaveDataFromFsForTest`, `WriteSaveDataToFsForTest` and `DeleteSaveDataOfFsForTest` according to RE.
---
 Ryujinx.Common/Logging/LogClass.cs            |   4 +-
 .../Bluetooth/BluetoothEventManager.cs        |  25 ++
 .../Services/Bluetooth/IBluetoothDriver.cs    |  91 ++++++
 .../HOS/Services/Bluetooth/IBluetoothUser.cs  |  29 ++
 Ryujinx.HLE/HOS/Services/Btm/IBtm.cs          |   8 +
 Ryujinx.HLE/HOS/Services/Btm/IBtmUser.cs      |  17 ++
 Ryujinx.HLE/HOS/Services/Btm/IBtmUserCore.cs  | 128 +++++++++
 Ryujinx.HLE/HOS/Services/Btm/ResultCode.cs    |  10 +
 Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs    |  21 ++
 Ryujinx.HLE/HOS/Services/Nsd/FqdnResolver.cs  | 131 +++++++++
 Ryujinx.HLE/HOS/Services/Nsd/IManager.cs      | 267 ++++++++++++++++++
 Ryujinx.HLE/HOS/Services/Nsd/NsdSettings.cs   |   9 +
 Ryujinx.HLE/HOS/Services/Nsd/ResultCode.cs    |  19 ++
 13 files changed, 758 insertions(+), 1 deletion(-)
 create mode 100644 Ryujinx.HLE/HOS/Services/Bluetooth/BluetoothEventManager.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Bluetooth/IBluetoothDriver.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Bluetooth/IBluetoothUser.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Btm/IBtm.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Btm/IBtmUser.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Btm/IBtmUserCore.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Btm/ResultCode.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Nsd/FqdnResolver.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Nsd/IManager.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Nsd/NsdSettings.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Nsd/ResultCode.cs

diff --git a/Ryujinx.Common/Logging/LogClass.cs b/Ryujinx.Common/Logging/LogClass.cs
index 66a83b376b..b056f3836b 100644
--- a/Ryujinx.Common/Logging/LogClass.cs
+++ b/Ryujinx.Common/Logging/LogClass.cs
@@ -20,6 +20,7 @@ namespace Ryujinx.Common.Logging
         ServiceApm,
         ServiceAudio,
         ServiceBsd,
+        ServiceBtm,
         ServiceCaps,
         ServiceFriend,
         ServiceFs,
@@ -31,6 +32,7 @@ namespace Ryujinx.Common.Logging
         ServiceNfp,
         ServiceNifm,
         ServiceNs,
+        ServiceNsd,
         ServiceNv,
         ServicePctl,
         ServicePl,
@@ -44,4 +46,4 @@ namespace Ryujinx.Common.Logging
         ServiceTime,
         ServiceVi
     }
-}
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Bluetooth/BluetoothEventManager.cs b/Ryujinx.HLE/HOS/Services/Bluetooth/BluetoothEventManager.cs
new file mode 100644
index 0000000000..9b7ca4c1dd
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Bluetooth/BluetoothEventManager.cs
@@ -0,0 +1,25 @@
+using Ryujinx.HLE.HOS.Kernel.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Bluetooth
+{
+    static class BluetoothEventManager
+    {
+        public static KEvent InitializeBleDebugEvent;
+        public static int    InitializeBleDebugEventHandle;
+
+        public static KEvent UnknownBleDebugEvent;
+        public static int    UnknownBleDebugEventHandle;
+
+        public static KEvent RegisterBleDebugEvent;
+        public static int    RegisterBleDebugEventHandle;
+
+        public static KEvent InitializeBleEvent;
+        public static int    InitializeBleEventHandle;
+
+        public static KEvent UnknownBleEvent;
+        public static int    UnknownBleEventHandle;
+
+        public static KEvent RegisterBleEvent;
+        public static int    RegisterBleEventHandle;
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Bluetooth/IBluetoothDriver.cs b/Ryujinx.HLE/HOS/Services/Bluetooth/IBluetoothDriver.cs
new file mode 100644
index 0000000000..4cee67cddf
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Bluetooth/IBluetoothDriver.cs
@@ -0,0 +1,91 @@
+using Ryujinx.HLE.HOS.Kernel.Common;
+using Ryujinx.HLE.HOS.Kernel.Threading;
+using Ryujinx.HLE.HOS.Services.Set;
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Bluetooth
+{
+    [Service("btdrv")]
+    class IBluetoothDriver : IpcService
+    {
+        private string _unknownLowEnergy;
+
+        public IBluetoothDriver(ServiceCtx context) { }
+
+        [Command(46)]
+        // InitializeBluetoothLe() -> handle<copy>
+        public ResultCode InitializeBluetoothLe(ServiceCtx context)
+        {
+            NxSettings.Settings.TryGetValue("bluetooth_debug!skip_boot", out object debugMode);
+
+            if ((bool)debugMode)
+            {
+                if (BluetoothEventManager.InitializeBleDebugEventHandle == 0)
+                {
+                    BluetoothEventManager.InitializeBleDebugEvent = new KEvent(context.Device.System);
+
+                    if (context.Process.HandleTable.GenerateHandle(BluetoothEventManager.InitializeBleDebugEvent.ReadableEvent, out BluetoothEventManager.InitializeBleDebugEventHandle) != KernelResult.Success)
+                    {
+                        throw new InvalidOperationException("Out of handles!");
+                    }
+                }
+
+                if (BluetoothEventManager.UnknownBleDebugEventHandle == 0)
+                {
+                    BluetoothEventManager.UnknownBleDebugEvent = new KEvent(context.Device.System);
+
+                    if (context.Process.HandleTable.GenerateHandle(BluetoothEventManager.UnknownBleDebugEvent.ReadableEvent, out BluetoothEventManager.UnknownBleDebugEventHandle) != KernelResult.Success)
+                    {
+                        throw new InvalidOperationException("Out of handles!");
+                    }
+                }
+
+                if (BluetoothEventManager.RegisterBleDebugEventHandle == 0)
+                {
+                    BluetoothEventManager.RegisterBleDebugEvent = new KEvent(context.Device.System);
+
+                    if (context.Process.HandleTable.GenerateHandle(BluetoothEventManager.RegisterBleDebugEvent.ReadableEvent, out BluetoothEventManager.RegisterBleDebugEventHandle) != KernelResult.Success)
+                    {
+                        throw new InvalidOperationException("Out of handles!");
+                    }
+                }
+            }
+            else
+            {
+                _unknownLowEnergy = "low_energy";
+
+                if (BluetoothEventManager.InitializeBleEventHandle == 0)
+                {
+                    BluetoothEventManager.InitializeBleEvent = new KEvent(context.Device.System);
+
+                    if (context.Process.HandleTable.GenerateHandle(BluetoothEventManager.InitializeBleEvent.ReadableEvent, out BluetoothEventManager.InitializeBleEventHandle) != KernelResult.Success)
+                    {
+                        throw new InvalidOperationException("Out of handles!");
+                    }
+                }
+
+                if (BluetoothEventManager.UnknownBleEventHandle == 0)
+                {
+                    BluetoothEventManager.UnknownBleEvent = new KEvent(context.Device.System);
+
+                    if (context.Process.HandleTable.GenerateHandle(BluetoothEventManager.UnknownBleEvent.ReadableEvent, out BluetoothEventManager.UnknownBleEventHandle) != KernelResult.Success)
+                    {
+                        throw new InvalidOperationException("Out of handles!");
+                    }
+                }
+
+                if (BluetoothEventManager.RegisterBleEventHandle == 0)
+                {
+                    BluetoothEventManager.RegisterBleEvent = new KEvent(context.Device.System);
+
+                    if (context.Process.HandleTable.GenerateHandle(BluetoothEventManager.RegisterBleEvent.ReadableEvent, out BluetoothEventManager.RegisterBleEventHandle) != KernelResult.Success)
+                    {
+                        throw new InvalidOperationException("Out of handles!");
+                    }
+                }
+            }
+
+            return ResultCode.Success;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Bluetooth/IBluetoothUser.cs b/Ryujinx.HLE/HOS/Services/Bluetooth/IBluetoothUser.cs
new file mode 100644
index 0000000000..d070e18f87
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Bluetooth/IBluetoothUser.cs
@@ -0,0 +1,29 @@
+using Ryujinx.HLE.HOS.Ipc;
+using Ryujinx.HLE.HOS.Services.Set;
+
+namespace Ryujinx.HLE.HOS.Services.Bluetooth
+{
+    [Service("bt")]
+    class IBluetoothUser : IpcService
+    {
+        public IBluetoothUser(ServiceCtx context) { }
+
+        [Command(9)]
+        // RegisterBleEvent(pid) -> handle<copy>
+        public ResultCode RegisterBleEvent(ServiceCtx context)
+        {
+            NxSettings.Settings.TryGetValue("bluetooth_debug!skip_boot", out object debugMode);
+
+            if ((bool)debugMode)
+            {
+                context.Response.HandleDesc = IpcHandleDesc.MakeCopy(BluetoothEventManager.RegisterBleDebugEventHandle);
+            }
+            else
+            {
+                context.Response.HandleDesc = IpcHandleDesc.MakeCopy(BluetoothEventManager.RegisterBleEventHandle);
+            }
+
+            return ResultCode.Success;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Btm/IBtm.cs b/Ryujinx.HLE/HOS/Services/Btm/IBtm.cs
new file mode 100644
index 0000000000..947fee14ca
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Btm/IBtm.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.HLE.HOS.Services.Btm
+{
+    [Service("btm")]
+    class IBtm : IpcService
+    {
+        public IBtm(ServiceCtx context) { }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Btm/IBtmUser.cs b/Ryujinx.HLE/HOS/Services/Btm/IBtmUser.cs
new file mode 100644
index 0000000000..133ef0cc2c
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Btm/IBtmUser.cs
@@ -0,0 +1,17 @@
+namespace Ryujinx.HLE.HOS.Services.Btm
+{
+    [Service("btm:u")]
+    class IBtmUser : IpcService
+    {
+        public IBtmUser(ServiceCtx context) { }
+
+        [Command(0)] // 5.0.0+
+        // GetCore() -> object<nn::btm::IBtmUserCore>
+        public ResultCode GetCore(ServiceCtx context)
+        {
+            MakeObject(context, new IBtmUserCore());
+
+            return ResultCode.Success;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Btm/IBtmUserCore.cs b/Ryujinx.HLE/HOS/Services/Btm/IBtmUserCore.cs
new file mode 100644
index 0000000000..14b7b5f35d
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Btm/IBtmUserCore.cs
@@ -0,0 +1,128 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Ipc;
+using Ryujinx.HLE.HOS.Kernel.Common;
+using Ryujinx.HLE.HOS.Kernel.Threading;
+
+namespace Ryujinx.HLE.HOS.Services.Btm
+{
+    class IBtmUserCore : IpcService
+    {
+        public KEvent _bleScanEvent;
+        public int    _bleScanEventHandle;
+
+        public KEvent _bleConnectionEvent;
+        public int    _bleConnectionEventHandle;
+
+        public KEvent _bleServiceDiscoveryEvent;
+        public int    _bleServiceDiscoveryEventHandle;
+
+        public KEvent _bleMtuConfigEvent;
+        public int    _bleMtuConfigEventHandle;
+
+        public IBtmUserCore() { }
+
+        [Command(0)] // 5.0.0+
+        // AcquireBleScanEvent() -> (byte<1>, handle<copy>)
+        public ResultCode AcquireBleScanEvent(ServiceCtx context)
+        {
+            KernelResult result = KernelResult.Success;
+
+            if (_bleScanEventHandle == 0)
+            {
+                _bleScanEvent = new KEvent(context.Device.System);
+
+                result = context.Process.HandleTable.GenerateHandle(_bleScanEvent.ReadableEvent, out _bleScanEventHandle);
+
+                if (result != KernelResult.Success)
+                {
+                    // NOTE: We use a Logging instead of an exception because the call return a boolean if succeed or not.
+                    Logger.PrintError(LogClass.ServiceBsd, "Out of handles!");
+                }
+            }
+
+            context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_bleScanEventHandle);
+
+            context.ResponseData.Write(result == KernelResult.Success ? 1 : 0);
+
+            return ResultCode.Success;
+        }
+
+        [Command(17)] // 5.0.0+
+        // AcquireBleConnectionEvent() -> (byte<1>, handle<copy>)
+        public ResultCode AcquireBleConnectionEvent(ServiceCtx context)
+        {
+            KernelResult result = KernelResult.Success;
+
+            if (_bleConnectionEventHandle == 0)
+            {
+                _bleConnectionEvent = new KEvent(context.Device.System);
+
+                result = context.Process.HandleTable.GenerateHandle(_bleConnectionEvent.ReadableEvent, out _bleConnectionEventHandle);
+
+                if (result != KernelResult.Success)
+                {
+                    // NOTE: We use a Logging instead of an exception because the call return a boolean if succeed or not.
+                    Logger.PrintError(LogClass.ServiceBsd, "Out of handles!");
+                }
+            }
+
+            context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_bleConnectionEventHandle);
+
+            context.ResponseData.Write(result == KernelResult.Success ? 1 : 0);
+
+            return ResultCode.Success;
+        }
+
+        [Command(26)] // 5.0.0+
+        // AcquireBleServiceDiscoveryEvent() -> (byte<1>, handle<copy>)
+        public ResultCode AcquireBleServiceDiscoveryEvent(ServiceCtx context)
+        {
+            KernelResult result = KernelResult.Success;
+
+            if (_bleServiceDiscoveryEventHandle == 0)
+            {
+                _bleServiceDiscoveryEvent = new KEvent(context.Device.System);
+
+                result = context.Process.HandleTable.GenerateHandle(_bleServiceDiscoveryEvent.ReadableEvent, out _bleServiceDiscoveryEventHandle);
+
+                if (result != KernelResult.Success)
+                {
+                    // NOTE: We use a Logging instead of an exception because the call return a boolean if succeed or not.
+                    Logger.PrintError(LogClass.ServiceBsd, "Out of handles!");
+                }
+            }
+
+            context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_bleServiceDiscoveryEventHandle);
+
+            context.ResponseData.Write(result == KernelResult.Success ? 1 : 0);
+
+            return ResultCode.Success;
+        }
+
+        [Command(33)] // 5.0.0+
+        // AcquireBleMtuConfigEvent() -> (byte<1>, handle<copy>)
+        public ResultCode AcquireBleMtuConfigEvent(ServiceCtx context)
+        {
+            KernelResult result = KernelResult.Success;
+
+            if (_bleMtuConfigEventHandle == 0)
+            {
+                _bleMtuConfigEvent = new KEvent(context.Device.System);
+
+                result = context.Process.HandleTable.GenerateHandle(_bleMtuConfigEvent.ReadableEvent, out _bleMtuConfigEventHandle);
+
+                if (result != KernelResult.Success)
+                {
+                    // NOTE: We use a Logging instead of an exception because the call return a boolean if succeed or not.
+                    Logger.PrintError(LogClass.ServiceBsd, "Out of handles!");
+                }
+            }
+
+            context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_bleMtuConfigEventHandle);
+
+            context.ResponseData.Write(result == KernelResult.Success ? 1 : 0);
+
+            return ResultCode.Success;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Btm/ResultCode.cs b/Ryujinx.HLE/HOS/Services/Btm/ResultCode.cs
new file mode 100644
index 0000000000..b222fdc88b
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Btm/ResultCode.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.HLE.HOS.Services.Btm
+{
+    enum ResultCode
+    {
+        ModuleId       = 143,
+        ErrorCodeShift = 9,
+
+        Success = 0
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs b/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
index 7d77ff32b4..9adc08c190 100644
--- a/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
+++ b/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
@@ -1413,6 +1413,27 @@ namespace Ryujinx.HLE.HOS.Services.Hid
             return ResultCode.Success;
         }
 
+        [Command(522)] // 5.1.0+
+        // SetIsPalmaAllConnectable(nn::applet::AppletResourceUserId, bool, pid)
+        public ResultCode SetIsPalmaAllConnectable(ServiceCtx context)
+        {
+            long appletResourceUserId = context.RequestData.ReadInt64();
+            long unknownBool          = context.RequestData.ReadInt64();
+
+            Logger.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, unknownBool });
+
+            return ResultCode.Success;
+        }
+
+        [Command(525)] // 5.1.0+
+        // SetPalmaBoostMode(bool)
+        public ResultCode SetPalmaBoostMode(ServiceCtx context)
+        {
+            // NOTE: Stubbed in system module.
+
+            return ResultCode.Success;
+        }
+
         [Command(1000)]
         // SetNpadCommunicationMode(long CommunicationMode, nn::applet::AppletResourceUserId)
         public ResultCode SetNpadCommunicationMode(ServiceCtx context)
diff --git a/Ryujinx.HLE/HOS/Services/Nsd/FqdnResolver.cs b/Ryujinx.HLE/HOS/Services/Nsd/FqdnResolver.cs
new file mode 100644
index 0000000000..10b6433a20
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nsd/FqdnResolver.cs
@@ -0,0 +1,131 @@
+using System.Text;
+
+namespace Ryujinx.HLE.HOS.Services.Nsd
+{
+    class FqdnResolver
+    {
+        private const string _dummyAddress = "unknown.dummy.nintendo.net";
+
+        private NsdSettings _nsdSettings;
+
+        public FqdnResolver(NsdSettings nsdSettings)
+        {
+            _nsdSettings = nsdSettings;
+        }
+
+        public ResultCode GetSettingName(ServiceCtx context, out string settingName)
+        {
+            if (_nsdSettings.TestMode)
+            {
+                settingName = "";
+
+                return ResultCode.NotImplemented;
+            }
+            else
+            {
+                settingName = "";
+
+                if (true) // TODO: Determine field (struct + 0x2C)
+                {
+                    settingName = _nsdSettings.Environment;
+
+                    return ResultCode.Success;
+                }
+
+                return ResultCode.NullOutputObject;
+            }
+        }
+
+        public ResultCode GetEnvironmentIdentifier(ServiceCtx context, out string identifier)
+        {
+            if (_nsdSettings.TestMode)
+            {
+                identifier = "rre";
+
+                return ResultCode.NotImplemented;
+            }
+            else
+            {
+                identifier = _nsdSettings.Environment;
+            }
+
+            return ResultCode.Success;
+        }
+
+        public ResultCode Resolve(ServiceCtx context, string address, out string resolvedAddress)
+        {
+            if (address != "api.sect.srv.nintendo.net" || address != "conntest.nintendowifi.net")
+            {
+                // TODO: Load Environment from the savedata.
+                address = address.Replace("%", _nsdSettings.Environment);
+
+                resolvedAddress = "";
+
+                if (_nsdSettings == null)
+                {
+                    return ResultCode.SettingsNotInitialized;
+                }
+
+                if (!_nsdSettings.Initialized)
+                {
+                    return ResultCode.SettingsNotLoaded;
+                }
+
+                switch (address)
+                {
+                    case "e97b8a9d672e4ce4845ec6947cd66ef6-sb-api.accounts.nintendo.com": // dp1 environment
+                        resolvedAddress = "e97b8a9d672e4ce4845ec6947cd66ef6-sb.baas.nintendo.com";
+                        break;
+                    case "api.accounts.nintendo.com": // dp1 environment
+                        resolvedAddress = "e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com";
+                        break;
+                    case "e97b8a9d672e4ce4845ec6947cd66ef6-sb.accounts.nintendo.com": // lp1 environment
+                        resolvedAddress = "e97b8a9d672e4ce4845ec6947cd66ef6-sb.baas.nintendo.com";
+                        break;
+                    case "accounts.nintendo.com": // lp1 environment
+                        resolvedAddress = "e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com";
+                        break;
+                    /*
+                    // TODO: Determine fields of the struct.
+                    case "": // + 0xEB8 || + 0x2BE8
+                        resolvedAddress = ""; // + 0xEB8 + 0x300 || + 0x2BE8 + 0x300
+                        break;
+                    */
+                    default:
+                        resolvedAddress = address;
+                        break;
+                }
+            }
+            else
+            {
+                resolvedAddress = address;
+            }
+
+            return ResultCode.Success;
+        }
+
+        public ResultCode ResolveEx(ServiceCtx context, out ResultCode resultCode, out string resolvedAddress)
+        {
+            (long inputPosition, long inputSize)  = context.Request.GetBufferType0x21();
+
+            byte[] addressBuffer = context.Memory.ReadBytes(inputPosition, inputSize);
+            string address       = Encoding.UTF8.GetString(addressBuffer);
+
+            resultCode = Resolve(context, address, out resolvedAddress);
+
+            if (resultCode != ResultCode.Success)
+            {
+                resolvedAddress = _dummyAddress;
+            }
+
+            if (_nsdSettings.TestMode)
+            {
+                return ResultCode.Success;
+            }
+            else
+            {
+                return resultCode;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nsd/IManager.cs b/Ryujinx.HLE/HOS/Services/Nsd/IManager.cs
new file mode 100644
index 0000000000..5dac7cf449
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nsd/IManager.cs
@@ -0,0 +1,267 @@
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.Exceptions;
+using Ryujinx.HLE.HOS.Services.Set;
+using System.Text;
+
+namespace Ryujinx.HLE.HOS.Services.Nsd
+{
+    [Service("nsd:a")] // Max sessions: 5
+    [Service("nsd:u")] // Max sessions: 20
+    class IManager : IpcService
+    {
+        private NsdSettings  _nsdSettings;
+        private FqdnResolver _fqdnResolver;
+
+        private bool _isInitialized = false;
+
+        public IManager(ServiceCtx context)
+        {
+            // TODO: Load nsd settings through the savedata 0x80000000000000B0 (nsdsave:/).
+
+            NxSettings.Settings.TryGetValue("nsd!test_mode", out object testMode);
+
+            _nsdSettings = new NsdSettings
+            {
+                Initialized = true,
+                TestMode    = (bool)testMode
+            };
+
+            _fqdnResolver = new FqdnResolver(_nsdSettings);
+
+            _isInitialized = true;
+        }
+
+        [Command(10)]
+        // GetSettingName() -> buffer<unknown<0x100>, 0x16>
+        public ResultCode GetSettingName(ServiceCtx context)
+        {
+            (long outputPosition, long outputSize) = context.Request.GetBufferType0x22();
+
+            ResultCode result = _fqdnResolver.GetSettingName(context, out string settingName);
+
+            if (result == ResultCode.Success)
+            {
+                byte[] settingNameBuffer = Encoding.UTF8.GetBytes(settingName + '\0');
+
+                context.Memory.WriteBytes(outputPosition, settingNameBuffer);
+            }
+
+            return result;
+        }
+
+        [Command(11)]
+        // GetEnvironmentIdentifier() -> buffer<unknown<8>, 0x16>
+        public ResultCode GetEnvironmentIdentifier(ServiceCtx context)
+        {
+            (long outputPosition, long outputSize) = context.Request.GetBufferType0x22();
+
+            ResultCode result = _fqdnResolver.GetEnvironmentIdentifier(context, out string identifier);
+
+            if (result == ResultCode.Success)
+            {
+                byte[] identifierBuffer = Encoding.UTF8.GetBytes(identifier + '\0');
+
+                context.Memory.WriteBytes(outputPosition, identifierBuffer);
+            }
+
+            return result;
+        }
+
+        [Command(12)]
+        // GetDeviceId() -> bytes<0x10, 1>
+        public ResultCode GetDeviceId(ServiceCtx context)
+        {
+            // NOTE: Stubbed in system module.
+
+            return ResultCode.Success;
+        }
+
+        [Command(13)]
+        // DeleteSettings(u32)
+        public ResultCode DeleteSettings(ServiceCtx context)
+        {
+            uint unknown = context.RequestData.ReadUInt32();
+
+            if (!_isInitialized)
+            {
+                return ResultCode.ServiceNotInitialized;
+            }
+
+            if (unknown > 1)
+            {
+                return ResultCode.InvalidArgument;
+            }
+
+            if (unknown == 1)
+            {
+                NxSettings.Settings.TryGetValue("nsd!environment_identifier", out object environmentIdentifier);
+
+                if ((string)environmentIdentifier == _nsdSettings.Environment)
+                {
+                    // TODO: Call nn::fs::DeleteSystemFile() to delete the savedata file and return ResultCode.
+                }
+                else
+                {
+                    // TODO: Mount the savedata file and return ResultCode.
+                }
+            }
+            else
+            {
+                // TODO: Call nn::fs::DeleteSystemFile() to delete the savedata file and return ResultCode.
+            }
+
+            return ResultCode.Success;
+        }
+
+        [Command(14)]
+        // ImportSettings(u32, buffer<unknown, 5>) -> buffer<unknown, 6>
+        public ResultCode ImportSettings(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+
+        [Command(15)]
+        // Unknown(bytes<1>)
+        public ResultCode Unknown(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+
+        [Command(20)]
+        // Resolve(buffer<unknown<0x100>, 0x15>) -> buffer<unknown<0x100>, 0x16>
+        public ResultCode Resolve(ServiceCtx context)
+        {
+            (long outputPosition, long outputSize) = context.Request.GetBufferType0x22();
+
+            ResultCode result = _fqdnResolver.ResolveEx(context, out ResultCode errorCode, out string resolvedAddress);
+
+            byte[] resolvedAddressBuffer = Encoding.UTF8.GetBytes(resolvedAddress + '\0');
+
+            context.Memory.WriteBytes(outputPosition, resolvedAddressBuffer);
+
+            return result;
+        }
+
+        [Command(21)]
+        // ResolveEx(buffer<unknown<0x100>, 0x15>) -> (u32, buffer<unknown<0x100>, 0x16>)
+        public ResultCode ResolveEx(ServiceCtx context)
+        {
+            (long outputPosition, long outputSize) = context.Request.GetBufferType0x22();
+
+            ResultCode result = _fqdnResolver.ResolveEx(context, out ResultCode errorCode, out string resolvedAddress);
+
+            byte[] resolvedAddressBuffer = Encoding.UTF8.GetBytes(resolvedAddress + '\0');
+
+            context.Memory.WriteBytes(outputPosition, resolvedAddressBuffer);
+
+            context.ResponseData.Write((int)errorCode);
+
+            return result;
+        }
+
+        [Command(30)]
+        // GetNasServiceSetting(buffer<unknown<0x10>, 0x15>) -> buffer<unknown<0x108>, 0x16>
+        public ResultCode GetNasServiceSetting(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+
+        [Command(31)]
+        // GetNasServiceSettingEx(buffer<unknown<0x10>, 0x15>) -> (u32, buffer<unknown<0x108>, 0x16>)
+        public ResultCode GetNasServiceSettingEx(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+
+        [Command(40)]
+        // GetNasRequestFqdn() -> buffer<unknown<0x100>, 0x16>
+        public ResultCode GetNasRequestFqdn(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+
+        [Command(41)]
+        // GetNasRequestFqdnEx() -> (u32, buffer<unknown<0x100>, 0x16>)
+        public ResultCode GetNasRequestFqdnEx(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+
+        [Command(42)]
+        // GetNasApiFqdn() -> buffer<unknown<0x100>, 0x16>
+        public ResultCode GetNasApiFqdn(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+
+        [Command(43)]
+        // GetNasApiFqdnEx() -> (u32, buffer<unknown<0x100>, 0x16>)
+        public ResultCode GetNasApiFqdnEx(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+
+        [Command(50)]
+        // GetCurrentSetting() -> buffer<unknown<0x12bf0>, 0x16>
+        public ResultCode GetCurrentSetting(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+
+        [Command(60)]
+        // ReadSaveDataFromFsForTest() -> buffer<unknown<0x12bf0>, 0x16>
+        public ResultCode ReadSaveDataFromFsForTest(ServiceCtx context)
+        {
+            if (!_isInitialized)
+            {
+                return ResultCode.ServiceNotInitialized;
+            }
+
+            // TODO: Call nn::nsd::detail::fs::ReadSaveDataWithOffset() at offset 0 to write the 
+            //       whole savedata inside the buffer.
+
+            Logger.PrintStub(LogClass.ServiceNsd);
+
+            return ResultCode.Success;
+        }
+
+        [Command(61)]
+        // WriteSaveDataToFsForTest(buffer<unknown<0x12bf0>, 0x15>)
+        public ResultCode WriteSaveDataToFsForTest(ServiceCtx context)
+        {
+            // NOTE: Stubbed in system module.
+
+            if (_isInitialized)
+            {
+                return ResultCode.NotImplemented;
+            }
+            else
+            {
+                return ResultCode.ServiceNotInitialized;
+            }
+        }
+
+        [Command(62)]
+        // DeleteSaveDataOfFsForTest()
+        public ResultCode DeleteSaveDataOfFsForTest(ServiceCtx context)
+        {
+            // NOTE: Stubbed in system module.
+
+            if (_isInitialized)
+            {
+                return ResultCode.NotImplemented;
+            }
+            else
+            {
+                return ResultCode.ServiceNotInitialized;
+            }
+        }
+
+        [Command(63)]
+        // IsChangeEnvironmentIdentifierDisabled() -> bytes<1>
+        public ResultCode IsChangeEnvironmentIdentifierDisabled(ServiceCtx context)
+        {
+            throw new ServiceNotImplementedException(context);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nsd/NsdSettings.cs b/Ryujinx.HLE/HOS/Services/Nsd/NsdSettings.cs
new file mode 100644
index 0000000000..42739df02c
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nsd/NsdSettings.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.HLE.HOS.Services.Nsd
+{
+    class NsdSettings
+    {
+        public bool   Initialized;
+        public bool   TestMode;
+        public string Environment = "lp1";  // or "dd1" if devkit.
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nsd/ResultCode.cs b/Ryujinx.HLE/HOS/Services/Nsd/ResultCode.cs
new file mode 100644
index 0000000000..27584eb1a2
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nsd/ResultCode.cs
@@ -0,0 +1,19 @@
+namespace Ryujinx.HLE.HOS.Services.Nsd
+{
+    enum ResultCode
+    {
+        ModuleId       = 141,
+        ErrorCodeShift = 9,
+
+        Success = 0,
+
+        NotImplemented         = (  1 << ErrorCodeShift) | ModuleId,
+        InvalidObject1         = (  3 << ErrorCodeShift) | ModuleId,
+        InvalidObject2         = (  4 << ErrorCodeShift) | ModuleId,
+        NullOutputObject       = (  5 << ErrorCodeShift) | ModuleId,
+        SettingsNotLoaded      = (  6 << ErrorCodeShift) | ModuleId,
+        InvalidArgument        = (  8 << ErrorCodeShift) | ModuleId,
+        SettingsNotInitialized = ( 10 << ErrorCodeShift) | ModuleId,
+        ServiceNotInitialized  = (400 << ErrorCodeShift) | ModuleId,
+    }
+}
\ No newline at end of file