aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Ryujinx.HLE/HOS/Horizon.cs33
-rw-r--r--Ryujinx.HLE/HOS/Services/Mii/Types/CoreData.cs14
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/ResultCode.cs9
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/IUser.cs798
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/AmiiboConstants.cs8
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/CommonInfo.cs17
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/Device.cs19
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceType.cs7
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/ModelInfo.cs16
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/MountTarget.cs9
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/NfpDevice.cs23
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/NfpDeviceState.cs (renamed from Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceState.cs)2
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/RegisterInfo.cs19
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/TagInfo.cs16
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/VirtualAmiiboFile.cs22
-rw-r--r--Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs205
-rw-r--r--Ryujinx/Ryujinx.csproj2
-rw-r--r--Ryujinx/Ui/MainWindow.cs59
-rw-r--r--Ryujinx/Ui/MainWindow.glade78
-rw-r--r--Ryujinx/Ui/Resources/Logo_Amiibo.pngbin0 -> 11676 bytes
-rw-r--r--Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs194
-rw-r--r--Ryujinx/Ui/Windows/AmiiboWindow.cs422
22 files changed, 1829 insertions, 143 deletions
diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs
index 16b4c376..4da147bf 100644
--- a/Ryujinx.HLE/HOS/Horizon.cs
+++ b/Ryujinx.HLE/HOS/Horizon.cs
@@ -22,6 +22,7 @@ using Ryujinx.HLE.HOS.Services.Apm;
using Ryujinx.HLE.HOS.Services.Arp;
using Ryujinx.HLE.HOS.Services.Audio.AudioRenderer;
using Ryujinx.HLE.HOS.Services.Mii;
+using Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager;
using Ryujinx.HLE.HOS.Services.Nv;
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
using Ryujinx.HLE.HOS.Services.Pcv.Bpc;
@@ -33,6 +34,7 @@ using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Utilities;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
@@ -65,6 +67,8 @@ namespace Ryujinx.HLE.HOS
internal AppletStateMgr AppletState { get; private set; }
+ internal List<NfpDevice> NfpDevices { get; private set; }
+
internal ServerBase BsdServer { get; private set; }
internal ServerBase AudRenServer { get; private set; }
internal ServerBase AudOutServer { get; private set; }
@@ -113,6 +117,8 @@ namespace Ryujinx.HLE.HOS
PerformanceState = new PerformanceState();
+ NfpDevices = new List<NfpDevice>();
+
// Note: This is not really correct, but with HLE of services, the only memory
// region used that is used is Application, so we can use the other ones for anything.
KMemoryRegionManager region = KernelContext.MemoryRegions[(int)MemoryRegion.NvServices];
@@ -320,6 +326,33 @@ namespace Ryujinx.HLE.HOS
AppletState.MessageEvent.ReadableEvent.Signal();
}
+ public void ScanAmiibo(int nfpDeviceId, string amiiboId, bool useRandomUuid)
+ {
+ if (NfpDevices[nfpDeviceId].State == NfpDeviceState.SearchingForTag)
+ {
+ NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
+ NfpDevices[nfpDeviceId].AmiiboId = amiiboId;
+ NfpDevices[nfpDeviceId].UseRandomUuid = useRandomUuid;
+ }
+ }
+
+ public bool SearchingForAmiibo(out int nfpDeviceId)
+ {
+ nfpDeviceId = default;
+
+ for (int i = 0; i < NfpDevices.Count; i++)
+ {
+ if (NfpDevices[i].State == NfpDeviceState.SearchingForTag)
+ {
+ nfpDeviceId = i;
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
public void SignalDisplayResolutionChange()
{
DisplayResolutionChangeEvent.ReadableEvent.Signal();
diff --git a/Ryujinx.HLE/HOS/Services/Mii/Types/CoreData.cs b/Ryujinx.HLE/HOS/Services/Mii/Types/CoreData.cs
index 1b11f99d..39a3945b 100644
--- a/Ryujinx.HLE/HOS/Services/Mii/Types/CoreData.cs
+++ b/Ryujinx.HLE/HOS/Services/Mii/Types/CoreData.cs
@@ -389,7 +389,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
coreData.SetDefault();
- if (gender == Types.Gender.All)
+ if (gender == Gender.All)
{
gender = (Gender)utilImpl.GetRandom((int)gender);
}
@@ -432,7 +432,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
int axisY = 0;
- if (gender == Types.Gender.Female && age == Age.Young)
+ if (gender == Gender.Female && age == Age.Young)
{
axisY = utilImpl.GetRandom(3);
}
@@ -466,8 +466,8 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
// Eye
coreData.EyeType = (EyeType)eyeTypeInfo.Values[utilImpl.GetRandom(eyeTypeInfo.ValuesCount)];
- int eyeRotateKey1 = gender != Types.Gender.Male ? 4 : 2;
- int eyeRotateKey2 = gender != Types.Gender.Male ? 3 : 4;
+ int eyeRotateKey1 = gender != Gender.Male ? 4 : 2;
+ int eyeRotateKey2 = gender != Gender.Male ? 3 : 4;
byte eyeRotateOffset = (byte)(32 - EyeRotateTable[eyeRotateKey1] + eyeRotateKey2);
byte eyeRotate = (byte)(32 - EyeRotateTable[(int)coreData.EyeType]);
@@ -496,14 +496,14 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
coreData.EyebrowY = (byte)(axisY + eyebrowY);
// Nose
- int noseScale = gender == Types.Gender.Female ? 3 : 4;
+ int noseScale = gender == Gender.Female ? 3 : 4;
coreData.NoseType = (NoseType)noseTypeInfo.Values[utilImpl.GetRandom(noseTypeInfo.ValuesCount)];
coreData.NoseScale = (byte)noseScale;
coreData.NoseY = (byte)(axisY + 9);
// Mouth
- int mouthColor = gender == Types.Gender.Female ? utilImpl.GetRandom(0, 4) : 0;
+ int mouthColor = gender == Gender.Female ? utilImpl.GetRandom(0, 4) : 0;
coreData.MouthType = (MouthType)mouthTypeInfo.Values[utilImpl.GetRandom(mouthTypeInfo.ValuesCount)];
coreData.MouthColor = (CommonColor)Helper.Ver3MouthColorTable[mouthColor];
@@ -515,7 +515,7 @@ namespace Ryujinx.HLE.HOS.Services.Mii.Types
coreData.BeardColor = coreData.HairColor;
coreData.MustacheScale = 4;
- if (gender == Types.Gender.Male && age != Age.Young && utilImpl.GetRandom(10) < 2)
+ if (gender == Gender.Male && age != Age.Young && utilImpl.GetRandom(10) < 2)
{
BeardAndMustacheFlag mustacheAndBeardFlag = (BeardAndMustacheFlag)utilImpl.GetRandom(3);
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/ResultCode.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/ResultCode.cs
index b42a28a9..e0ccbc6d 100644
--- a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/ResultCode.cs
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/ResultCode.cs
@@ -7,7 +7,12 @@
Success = 0,
- DeviceNotFound = (64 << ErrorCodeShift) | ModuleId,
- DevicesBufferIsNull = (65 << ErrorCodeShift) | ModuleId
+ DeviceNotFound = (64 << ErrorCodeShift) | ModuleId,
+ WrongArgument = (65 << ErrorCodeShift) | ModuleId,
+ WrongDeviceState = (73 << ErrorCodeShift) | ModuleId,
+ NfcDisabled = (80 << ErrorCodeShift) | ModuleId,
+ TagNotFound = (97 << ErrorCodeShift) | ModuleId,
+ ApplicationAreaIsNull = (128 << ErrorCodeShift) | ModuleId,
+ ApplicationAreaAlreadyCreated = (168 << ErrorCodeShift) | ModuleId
}
} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/IUser.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/IUser.cs
index 2cd35b9e..90881565 100644
--- a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/IUser.cs
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/IUser.cs
@@ -1,4 +1,6 @@
-using Ryujinx.HLE.Exceptions;
+using Ryujinx.Common.Memory;
+using Ryujinx.Cpu;
+using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Threading;
@@ -6,18 +8,25 @@ using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.Services.Hid.HidServer;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager;
using System;
-using System.Collections.Generic;
+using System.Buffers.Binary;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
class IUser : IpcService
{
+ private ulong _appletResourceUserId;
+ private ulong _mcuVersionData;
+ private byte[] _mcuData;
+
private State _state = State.NonInitialized;
private KEvent _availabilityChangeEvent;
- private int _availabilityChangeEventHandle = 0;
- private List<Device> _devices = new List<Device>();
+ private CancellationTokenSource _cancelTokenSource;
public IUser() { }
@@ -25,32 +34,30 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
// Initialize(u64, u64, pid, buffer<unknown, 5>)
public ResultCode Initialize(ServiceCtx context)
{
- long appletResourceUserId = context.RequestData.ReadInt64();
- long mcuVersionData = context.RequestData.ReadInt64();
+ _appletResourceUserId = context.RequestData.ReadUInt64();
+ _mcuVersionData = context.RequestData.ReadUInt64();
long inputPosition = context.Request.SendBuff[0].Position;
long inputSize = context.Request.SendBuff[0].Size;
- byte[] unknownBuffer = new byte[inputSize];
-
- context.Memory.Read((ulong)inputPosition, unknownBuffer);
+ _mcuData = new byte[inputSize];
- // NOTE: appletResourceUserId, mcuVersionData and the buffer are stored inside an internal struct.
- // The buffer seems to contains entries with a size of 0x40 bytes each.
- // Sadly, this internal struct doesn't seems to be used in retail.
+ context.Memory.Read((ulong)inputPosition, _mcuData);
- // TODO: Add an instance of nn::nfc::server::Manager when it will be implemented.
- // Add an instance of nn::nfc::server::SaveData when it will be implemented.
+ // TODO: The mcuData buffer seems to contains entries with a size of 0x40 bytes each. Usage of the data needs to be determined.
- // TODO: When we will be able to add multiple controllers add one entry by controller here.
- Device device1 = new Device
+ // TODO: Handle this in a controller class directly.
+ // Every functions which use the Handle call nn::hid::system::GetXcdHandleForNpadWithNfc().
+ NfpDevice devicePlayer1 = new NfpDevice
{
NpadIdType = NpadIdType.Player1,
Handle = HidUtils.GetIndexFromNpadIdType(NpadIdType.Player1),
- State = DeviceState.Initialized
+ State = NfpDeviceState.Initialized
};
- _devices.Add(device1);
+ context.Device.System.NfpDevices.Add(devicePlayer1);
+
+ // TODO: It mounts 0x8000000000000020 save data and stores a random generate value inside. Usage of the data needs to be determined.
_state = State.Initialized;
@@ -61,13 +68,18 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
// Finalize()
public ResultCode Finalize(ServiceCtx context)
{
- // TODO: Call StopDetection() and Unmount() when they will be implemented.
- // Remove the instance of nn::nfc::server::Manager when it will be implemented.
- // Remove the instance of nn::nfc::server::SaveData when it will be implemented.
+ if (_state == State.Initialized)
+ {
+ if (_cancelTokenSource != null)
+ {
+ _cancelTokenSource.Cancel();
+ }
- _devices.Clear();
+ // NOTE: All events are destroyed here.
+ context.Device.System.NfpDevices.Clear();
- _state = State.NonInitialized;
+ _state = State.NonInitialized;
+ }
return ResultCode.Success;
}
@@ -78,23 +90,32 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
if (context.Request.RecvListBuff.Count == 0)
{
- return ResultCode.DevicesBufferIsNull;
+ return ResultCode.WrongArgument;
}
long outputPosition = context.Request.RecvListBuff[0].Position;
- long outputSize = context.Request.RecvListBuff[0].Size;
+ long outputSize = context.Request.RecvListBuff[0].Size;
- if (_devices.Count == 0)
+ if (context.Device.System.NfpDevices.Count == 0)
{
return ResultCode.DeviceNotFound;
}
- for (int i = 0; i < _devices.Count; i++)
+ MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize);
+
+ if (CheckNfcIsEnabled() == ResultCode.Success)
{
- context.Memory.Write((ulong)(outputPosition + (i * sizeof(long))), (uint)_devices[i].Handle);
- }
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ context.Memory.Write((ulong)(outputPosition + (i * sizeof(long))), (uint)context.Device.System.NfpDevices[i].Handle);
+ }
- context.ResponseData.Write(_devices.Count);
+ context.ResponseData.Write(context.Device.System.NfpDevices.Count);
+ }
+ else
+ {
+ context.ResponseData.Write(0);
+ }
return ResultCode.Success;
}
@@ -103,56 +124,376 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
// StartDetection(bytes<8, 4>)
public ResultCode StartDetection(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ context.Device.System.NfpDevices[i].State = NfpDeviceState.SearchingForTag;
+
+ break;
+ }
+ }
+
+ _cancelTokenSource = new CancellationTokenSource();
+
+ Task.Run(() =>
+ {
+ while (true)
+ {
+ if (_cancelTokenSource.Token.IsCancellationRequested)
+ {
+ break;
+ }
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagFound)
+ {
+ context.Device.System.NfpDevices[i].SignalActivate();
+ Thread.Sleep(50); // NOTE: Simulate amiibo scanning delay.
+ context.Device.System.NfpDevices[i].SignalDeactivate();
+
+ break;
+ }
+ }
+ }
+ }, _cancelTokenSource.Token);
+
+ return ResultCode.Success;
}
[Command(4)]
// StopDetection(bytes<8, 4>)
public ResultCode StopDetection(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ if (_cancelTokenSource != null)
+ {
+ _cancelTokenSource.Cancel();
+ }
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ context.Device.System.NfpDevices[i].State = NfpDeviceState.Initialized;
+
+ break;
+ }
+ }
+
+ return ResultCode.Success;
}
[Command(5)]
// Mount(bytes<8, 4>, u32, u32)
public ResultCode Mount(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+ UserManager.DeviceType deviceType = (UserManager.DeviceType)context.RequestData.ReadUInt32();
+ MountTarget mountTarget = (MountTarget)context.RequestData.ReadUInt32();
+
+ if (deviceType != 0)
+ {
+ return ResultCode.WrongArgument;
+ }
+
+ if (((uint)mountTarget & 3) == 0)
+ {
+ return ResultCode.WrongArgument;
+ }
+
+ // TODO: Found how the MountTarget is handled.
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagFound)
+ {
+ // NOTE: This mount the amiibo data, which isn't needed in our case.
+
+ context.Device.System.NfpDevices[i].State = NfpDeviceState.TagMounted;
+
+ resultCode = ResultCode.Success;
+ }
+ else
+ {
+ resultCode = ResultCode.WrongDeviceState;
+ }
+ }
+
+ break;
+ }
+ }
+
+ return resultCode;
}
[Command(6)]
// Unmount(bytes<8, 4>)
public ResultCode Unmount(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ // NOTE: This mount the amiibo data, which isn't needed in our case.
+
+ context.Device.System.NfpDevices[i].State = NfpDeviceState.TagFound;
+
+ resultCode = ResultCode.Success;
+ }
+
+ break;
+ }
+ }
+
+ return resultCode;
}
[Command(7)]
// OpenApplicationArea(bytes<8, 4>, u32)
public ResultCode OpenApplicationArea(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ uint applicationAreaId = context.RequestData.ReadUInt32();
+
+ bool isOpened = false;
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagMounted)
+ {
+ isOpened = VirtualAmiibo.OpenApplicationArea(context.Device.System.NfpDevices[i].AmiiboId, applicationAreaId);
+
+ resultCode = ResultCode.Success;
+ }
+ else
+ {
+ resultCode = ResultCode.WrongDeviceState;
+ }
+ }
+
+ break;
+ }
+ }
+
+ if (!isOpened)
+ {
+ resultCode = ResultCode.ApplicationAreaIsNull;
+ }
+
+ return resultCode;
}
[Command(8)]
// GetApplicationArea(bytes<8, 4>) -> (u32, buffer<unknown, 6>)
public ResultCode GetApplicationArea(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ long outputPosition = context.Request.ReceiveBuff[0].Position;
+ long outputSize = context.Request.ReceiveBuff[0].Size;
+
+ MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize);
+
+ uint size = 0;
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagMounted)
+ {
+ byte[] applicationArea = VirtualAmiibo.GetApplicationArea(context.Device.System.NfpDevices[i].AmiiboId);
+
+ context.Memory.Write((ulong)outputPosition, applicationArea);
+
+ size = (uint)applicationArea.Length;
+
+ resultCode = ResultCode.Success;
+ }
+ else
+ {
+ resultCode = ResultCode.WrongDeviceState;
+ }
+ }
+ }
+ }
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ if (size == 0)
+ {
+ return ResultCode.ApplicationAreaIsNull;
+ }
+
+ context.ResponseData.Write(size);
+
+ return ResultCode.Success;
}
[Command(9)]
// SetApplicationArea(bytes<8, 4>, buffer<unknown, 5>)
public ResultCode SetApplicationArea(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ long inputPosition = context.Request.SendBuff[0].Position;
+ long inputSize = context.Request.SendBuff[0].Size;
+
+ byte[] applicationArea = new byte[inputSize];
+
+ context.Memory.Read((ulong)inputPosition, applicationArea);
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagMounted)
+ {
+ VirtualAmiibo.SetApplicationArea(context.Device.System.NfpDevices[i].AmiiboId, applicationArea);
+
+ resultCode = ResultCode.Success;
+ }
+ else
+ {
+ resultCode = ResultCode.WrongDeviceState;
+ }
+ }
+
+ break;
+ }
+ }
+
+ return resultCode;
}
[Command(10)]
// Flush(bytes<8, 4>)
public ResultCode Flush(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ // NOTE: Since we handle amiibo through VirtualAmiibo, we don't have to flush anything in our case.
+
+ return ResultCode.Success;
}
[Command(11)]
@@ -166,35 +507,328 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
// CreateApplicationArea(bytes<8, 4>, u32, buffer<unknown, 5>)
public ResultCode CreateApplicationArea(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ uint applicationAreaId = context.RequestData.ReadUInt32();
+
+ long inputPosition = context.Request.SendBuff[0].Position;
+ long inputSize = context.Request.SendBuff[0].Size;
+
+ byte[] applicationArea = new byte[inputSize];
+
+ context.Memory.Read((ulong)inputPosition, applicationArea);
+
+ bool isCreated = false;
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagMounted)
+ {
+ isCreated = VirtualAmiibo.CreateApplicationArea(context.Device.System.NfpDevices[i].AmiiboId, applicationAreaId, applicationArea);
+
+ resultCode = ResultCode.Success;
+ }
+ else
+ {
+ resultCode = ResultCode.WrongDeviceState;
+ }
+ }
+
+ break;
+ }
+ }
+
+ if (!isCreated)
+ {
+ resultCode = ResultCode.ApplicationAreaIsNull;
+ }
+
+ return resultCode;
}
[Command(13)]
// GetTagInfo(bytes<8, 4>) -> buffer<unknown<0x58>, 0x1a>
public ResultCode GetTagInfo(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ if (context.Request.RecvListBuff.Count == 0)
+ {
+ return ResultCode.WrongArgument;
+ }
+
+ long outputPosition = context.Request.RecvListBuff[0].Position;
+
+ context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(Marshal.SizeOf(typeof(TagInfo)));
+
+ MemoryHelper.FillWithZeros(context.Memory, outputPosition, Marshal.SizeOf(typeof(TagInfo)));
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagMounted || context.Device.System.NfpDevices[i].State == NfpDeviceState.TagFound)
+ {
+ byte[] Uuid = VirtualAmiibo.GenerateUuid(context.Device.System.NfpDevices[i].AmiiboId, context.Device.System.NfpDevices[i].UseRandomUuid);
+
+ if (Uuid.Length > AmiiboConstants.UuidMaxLength)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ TagInfo tagInfo = new TagInfo
+ {
+ UuidLength = (byte)Uuid.Length,
+ Reserved1 = new Array21<byte>(),
+ Protocol = uint.MaxValue, // All Protocol
+ TagType = uint.MaxValue, // All Type
+ Reserved2 = new Array6<byte>()
+ };
+
+ Uuid.CopyTo(tagInfo.Uuid.ToSpan());
+
+ context.Memory.Write((ulong)outputPosition, tagInfo);
+
+ resultCode = ResultCode.Success;
+ }
+ else
+ {
+ resultCode = ResultCode.WrongDeviceState;
+ }
+ }
+
+ break;
+ }
+ }
+
+ return resultCode;
}
[Command(14)]
// GetRegisterInfo(bytes<8, 4>) -> buffer<unknown<0x100>, 0x1a>
public ResultCode GetRegisterInfo(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ if (context.Request.RecvListBuff.Count == 0)
+ {
+ return ResultCode.WrongArgument;
+ }
+
+ long outputPosition = context.Request.RecvListBuff[0].Position;
+
+ context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(Marshal.SizeOf(typeof(RegisterInfo)));
+
+ MemoryHelper.FillWithZeros(context.Memory, outputPosition, Marshal.SizeOf(typeof(RegisterInfo)));
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagMounted)
+ {
+ RegisterInfo registerInfo = VirtualAmiibo.GetRegisterInfo(context.Device.System.NfpDevices[i].AmiiboId);
+
+ context.Memory.Write((ulong)outputPosition, registerInfo);
+
+ resultCode = ResultCode.Success;
+ }
+ else
+ {
+ resultCode = ResultCode.WrongDeviceState;
+ }
+ }
+
+ break;
+ }
+ }
+
+ return resultCode;
}
[Command(15)]
// GetCommonInfo(bytes<8, 4>) -> buffer<unknown<0x40>, 0x1a>
public ResultCode GetCommonInfo(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ if (context.Request.RecvListBuff.Count == 0)
+ {
+ return ResultCode.WrongArgument;
+ }
+
+ long outputPosition = context.Request.RecvListBuff[0].Position;
+
+ context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(Marshal.SizeOf(typeof(CommonInfo)));
+
+ MemoryHelper.FillWithZeros(context.Memory, outputPosition, Marshal.SizeOf(typeof(CommonInfo)));
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagMounted)
+ {
+ CommonInfo commonInfo = VirtualAmiibo.GetCommonInfo(context.Device.System.NfpDevices[i].AmiiboId);
+
+ context.Memory.Write((ulong)outputPosition, commonInfo);
+
+ resultCode = ResultCode.Success;
+ }
+ else
+ {
+ resultCode = ResultCode.WrongDeviceState;
+ }
+ }
+
+ break;
+ }
+ }
+
+ return resultCode;
}
[Command(16)]
// GetModelInfo(bytes<8, 4>) -> buffer<unknown<0x40>, 0x1a>
public ResultCode GetModelInfo(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ ResultCode resultCode = CheckNfcIsEnabled();
+
+ if (resultCode != ResultCode.Success)
+ {
+ return resultCode;
+ }
+
+ if (context.Request.RecvListBuff.Count == 0)
+ {
+ return ResultCode.WrongArgument;
+ }
+
+ long outputPosition = context.Request.RecvListBuff[0].Position;
+
+ context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize(Marshal.SizeOf(typeof(ModelInfo)));
+
+ MemoryHelper.FillWithZeros(context.Memory, outputPosition, Marshal.SizeOf(typeof(ModelInfo)));
+
+ uint deviceHandle = (uint)context.RequestData.ReadUInt64();
+
+ if (context.Device.System.NfpDevices.Count == 0)
+ {
+ return ResultCode.DeviceNotFound;
+ }
+
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
+ {
+ if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagRemoved)
+ {
+ resultCode = ResultCode.TagNotFound;
+ }
+ else
+ {
+ if (context.Device.System.NfpDevices[i].State == NfpDeviceState.TagMounted)
+ {
+ ModelInfo modelInfo = new ModelInfo
+ {
+ Reserved = new Array57<byte>()
+ };
+
+ modelInfo.CharacterId = BinaryPrimitives.ReverseEndianness(ushort.Parse(context.Device.System.NfpDevices[i].AmiiboId.Substring(0, 4), NumberStyles.HexNumber));
+ modelInfo.CharacterVariant = byte.Parse(context.Device.System.NfpDevices[i].AmiiboId.Substring(4, 2), NumberStyles.HexNumber);
+ modelInfo.Series = byte.Parse(context.Device.System.NfpDevices[i].AmiiboId.Substring(12, 2), NumberStyles.HexNumber);
+ modelInfo.ModelNumber = ushort.Parse(context.Device.System.NfpDevices[i].AmiiboId.Substring(8, 4), NumberStyles.HexNumber);
+ modelInfo.Type = byte.Parse(context.Device.System.NfpDevices[i].AmiiboId.Substring(6, 2), NumberStyles.HexNumber);
+
+ context.Memory.Write((ulong)outputPosition, modelInfo);
+
+ resultCode = ResultCode.Success;
+ }
+ else
+ {
+ resultCode = ResultCode.WrongDeviceState;
+ }
+ }
+
+ break;
+ }
+ }
+
+ return resultCode;
}
[Command(17)]
@@ -203,21 +837,18 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
uint deviceHandle = context.RequestData.ReadUInt32();
- for (int i = 0; i < _devices.Count; i++)
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
{
- if ((uint)_devices[i].Handle == deviceHandle)
+ if ((uint)context.Device.System.NfpDevices[i].Handle == deviceHandle)
{
- if (_devices[i].ActivateEventHandle == 0)
- {
- _devices[i].ActivateEvent = new KEvent(context.Device.System.KernelContext);
+ context.Device.System.NfpDevices[i].ActivateEvent = new KEvent(context.Device.System.KernelContext);
- if (context.Process.HandleTable.GenerateHandle(_devices[i].ActivateEvent.ReadableEvent, out _devices[i].ActivateEventHandle) != KernelResult.Success)
- {
- throw new InvalidOperationException("Out of handles!");
- }
+ if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfpDevices[i].ActivateEvent.ReadableEvent, out int activateEventHandle) != KernelResult.Success)
+ {
+ throw new InvalidOperationException("Out of handles!");
}
- context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_devices[i].ActivateEventHandle);
+ context.Response.HandleDesc = IpcHandleDesc.MakeCopy(activateEventHandle);
return ResultCode.Success;
}
@@ -232,21 +863,18 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
uint deviceHandle = context.RequestData.ReadUInt32();
- for (int i = 0; i < _devices.Count; i++)
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
{
- if ((uint)_devices[i].Handle == deviceHandle)
+ if ((uint)context.Device.System.NfpDevices[i].Handle == deviceHandle)
{
- if (_devices[i].DeactivateEventHandle == 0)
- {
- _devices[i].DeactivateEvent = new KEvent(context.Device.System.KernelContext);
+ context.Device.System.NfpDevices[i].DeactivateEvent = new KEvent(context.Device.System.KernelContext);
- if (context.Process.HandleTable.GenerateHandle(_devices[i].DeactivateEvent.ReadableEvent, out _devices[i].DeactivateEventHandle) != KernelResult.Success)
- {
- throw new InvalidOperationException("Out of handles!");
- }
+ if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfpDevices[i].DeactivateEvent.ReadableEvent, out int deactivateEventHandle) != KernelResult.Success)
+ {
+ throw new InvalidOperationException("Out of handles!");
}
- context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_devices[i].DeactivateEventHandle);
+ context.Response.HandleDesc = IpcHandleDesc.MakeCopy(deactivateEventHandle);
return ResultCode.Success;
}
@@ -270,17 +898,22 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
uint deviceHandle = context.RequestData.ReadUInt32();
- for (int i = 0; i < _devices.Count; i++)
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
{
- if ((uint)_devices[i].Handle == deviceHandle)
+ if ((uint)context.Device.System.NfpDevices[i].Handle == deviceHandle)
{
- context.ResponseData.Write((uint)_devices[i].State);
+ if (context.Device.System.NfpDevices[i].State > NfpDeviceState.Finalized)
+ {
+ throw new ArgumentOutOfRangeException();
+ }
+
+ context.ResponseData.Write((uint)context.Device.System.NfpDevices[i].State);
return ResultCode.Success;
}
}
- context.ResponseData.Write((uint)DeviceState.Unavailable);
+ context.ResponseData.Write((uint)NfpDeviceState.Unavailable);
return ResultCode.DeviceNotFound;
}
@@ -291,11 +924,11 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
uint deviceHandle = context.RequestData.ReadUInt32();
- for (int i = 0; i < _devices.Count; i++)
+ for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
{
- if ((uint)_devices[i].Handle == deviceHandle)
+ if ((uint)context.Device.System.NfpDevices[i].Handle == deviceHandle)
{
- context.ResponseData.Write((uint)HidUtils.GetNpadIdTypeFromIndex(_devices[i].Handle));
+ context.ResponseData.Write((uint)HidUtils.GetNpadIdTypeFromIndex(context.Device.System.NfpDevices[i].Handle));
return ResultCode.Success;
}
@@ -305,27 +938,26 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
}
[Command(22)]
- // GetApplicationAreaSize(bytes<8, 4>) -> u32
+ // GetApplicationAreaSize() -> u32
public ResultCode GetApplicationAreaSize(ServiceCtx context)
{
- throw new ServiceNotImplementedException(this, context);
+ context.ResponseData.Write(AmiiboConstants.ApplicationAreaSize);
+
+ return ResultCode.Success;
}
[Command(23)] // 3.0.0+
// AttachAvailabilityChangeEvent() -> handle<copy>
public ResultCode AttachAvailabilityChangeEvent(ServiceCtx context)
{
- if (_availabilityChangeEventHandle == 0)
- {
- _availabilityChangeEvent = new KEvent(context.Device.System.KernelContext);
+ _availabilityChangeEvent = new KEvent(context.Device.System.KernelContext);
- if (context.Process.HandleTable.GenerateHandle(_availabilityChangeEvent.ReadableEvent, out _availabilityChangeEventHandle) != KernelResult.Success)
- {
- throw new InvalidOperationException("Out of handles!");
- }
+ if (context.Process.HandleTable.GenerateHandle(_availabilityChangeEvent.ReadableEvent, out int availabilityChangeEventHandle) != KernelResult.Success)
+ {
+ throw new InvalidOperationException("Out of handles!");
}
- context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_availabilityChangeEventHandle);
+ context.Response.HandleDesc = IpcHandleDesc.MakeCopy(availabilityChangeEventHandle);
return ResultCode.Success;
}
@@ -336,5 +968,11 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
throw new ServiceNotImplementedException(this, context);
}
+
+ private ResultCode CheckNfcIsEnabled()
+ {
+ // TODO: Call nn::settings::detail::GetNfcEnableFlag when it will be implemented.
+ return true ? ResultCode.Success : ResultCode.NfcDisabled;
+ }
}
} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/AmiiboConstants.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/AmiiboConstants.cs
new file mode 100644
index 00000000..47f6f0fa
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/AmiiboConstants.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
+{
+ static class AmiiboConstants
+ {
+ public const int UuidMaxLength = 10;
+ public const int ApplicationAreaSize = 0xD8;
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/CommonInfo.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/CommonInfo.cs
new file mode 100644
index 00000000..da055dc3
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/CommonInfo.cs
@@ -0,0 +1,17 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x40)]
+ struct CommonInfo
+ {
+ public ushort LastWriteYear;
+ public byte LastWriteMonth;
+ public byte LastWriteDay;
+ public ushort WriteCounter;
+ public ushort Version;
+ public uint ApplicationAreaSize;
+ public Array52<byte> Reserved;
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/Device.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/Device.cs
deleted file mode 100644
index 3ff3489b..00000000
--- a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/Device.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using Ryujinx.HLE.HOS.Kernel.Threading;
-using Ryujinx.HLE.HOS.Services.Hid;
-
-namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
-{
- class Device
- {
- public KEvent ActivateEvent;
- public int ActivateEventHandle;
-
- public KEvent DeactivateEvent;
- public int DeactivateEventHandle;
-
- public DeviceState State = DeviceState.Unavailable;
-
- public PlayerIndex Handle;
- public NpadIdType NpadIdType;
- }
-} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceType.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceType.cs
new file mode 100644
index 00000000..753b91a9
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceType.cs
@@ -0,0 +1,7 @@
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
+{
+ enum DeviceType : uint
+ {
+ Amiibo
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/ModelInfo.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/ModelInfo.cs
new file mode 100644
index 00000000..1b6a3d32
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/ModelInfo.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x40)]
+ struct ModelInfo
+ {
+ public ushort CharacterId;
+ public byte CharacterVariant;
+ public byte Series;
+ public ushort ModelNumber;
+ public byte Type;
+ public Array57<byte> Reserved;
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/MountTarget.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/MountTarget.cs
new file mode 100644
index 00000000..11520bc6
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/MountTarget.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
+{
+ enum MountTarget : uint
+ {
+ Rom = 1,
+ Ram = 2,
+ All = 3
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/NfpDevice.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/NfpDevice.cs
new file mode 100644
index 00000000..b0d9c806
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/NfpDevice.cs
@@ -0,0 +1,23 @@
+using Ryujinx.HLE.HOS.Kernel.Threading;
+using Ryujinx.HLE.HOS.Services.Hid;
+
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
+{
+ class NfpDevice
+ {
+ public KEvent ActivateEvent;
+ public KEvent DeactivateEvent;
+
+ public void SignalActivate() => ActivateEvent.ReadableEvent.Signal();
+ public void SignalDeactivate() => DeactivateEvent.ReadableEvent.Signal();
+
+ public NfpDeviceState State = NfpDeviceState.Unavailable;
+
+ public PlayerIndex Handle;
+ public NpadIdType NpadIdType;
+
+ public string AmiiboId;
+
+ public bool UseRandomUuid;
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceState.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/NfpDeviceState.cs
index 7e373494..0e753250 100644
--- a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceState.cs
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/NfpDeviceState.cs
@@ -1,6 +1,6 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
{
- enum DeviceState
+ enum NfpDeviceState
{
Initialized = 0,
SearchingForTag = 1,
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/RegisterInfo.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/RegisterInfo.cs
new file mode 100644
index 00000000..3c72a971
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/RegisterInfo.cs
@@ -0,0 +1,19 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.HLE.HOS.Services.Mii.Types;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x100)]
+ struct RegisterInfo
+ {
+ public CharInfo MiiCharInfo;
+ public ushort FirstWriteYear;
+ public byte FirstWriteMonth;
+ public byte FirstWriteDay;
+ public Array11<byte> Nickname;
+ public byte FontRegion;
+ public Array64<byte> Reserved1;
+ public Array58<byte> Reserved2;
+ }
+} \ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/TagInfo.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/TagInfo.cs
new file mode 100644
index 00000000..950f8c10
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/TagInfo.cs
@@ -0,0 +1,16 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
+{
+ [StructLayout(LayoutKind.Sequential, Size = 0x58)]
+ struct TagInfo
+ {
+ public Array10<byte> Uuid;
+ public byte UuidLength;
+ public Array21<byte> Reserved1;
+ public uint Protocol;
+ public uint TagType;
+ public Array6<byte> Reserved2;
+ }
+}
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/VirtualAmiiboFile.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/VirtualAmiiboFile.cs
new file mode 100644
index 00000000..5265c038
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/VirtualAmiiboFile.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
+{
+ struct VirtualAmiiboFile
+ {
+ public uint FileVersion { get; set; }
+ public byte[] TagUuid { get; set; }
+ public string AmiiboId { get; set; }
+ public DateTime FirstWriteDate { get; set; }
+ public DateTime LastWriteDate { get; set; }
+ public ushort WriteCounter { get; set; }
+ public List<VirtualAmiiboApplicationArea> ApplicationAreas { get; set; }
+ }
+
+ struct VirtualAmiiboApplicationArea
+ {
+ public uint ApplicationAreaId { get; set; }
+ public byte[] ApplicationArea { get; set; }
+ }
+}
diff --git a/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs
new file mode 100644
index 00000000..bd810d96
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs
@@ -0,0 +1,205 @@
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Memory;
+using Ryujinx.HLE.HOS.Services.Mii;
+using Ryujinx.HLE.HOS.Services.Mii.Types;
+using Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+
+namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
+{
+ static class VirtualAmiibo
+ {
+ private static uint _openedApplicationAreaId;
+
+ public static byte[] GenerateUuid(string amiiboId, bool useRandomUuid)
+ {
+ if (useRandomUuid)
+ {
+ return GenerateRandomUuid();
+ }
+
+ VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
+
+ if (virtualAmiiboFile.TagUuid.Length == 0)
+ {
+ virtualAmiiboFile.TagUuid = GenerateRandomUuid();
+
+ SaveAmiiboFile(virtualAmiiboFile);
+ }
+
+ return virtualAmiiboFile.TagUuid;
+ }
+
+ private static byte[] GenerateRandomUuid()
+ {
+ byte[] uuid = new byte[9];
+
+ new Random().NextBytes(uuid);
+
+ uuid[3] = (byte)(0x88 ^ uuid[0] ^ uuid[1] ^ uuid[2]);
+ uuid[8] = (byte)(uuid[3] ^ uuid[4] ^ uuid[5] ^ uuid[6]);
+
+ return uuid;
+ }
+
+ public static CommonInfo GetCommonInfo(string amiiboId)
+ {
+ VirtualAmiiboFile amiiboFile = LoadAmiiboFile(amiiboId);
+
+ return new CommonInfo()
+ {
+ LastWriteYear = (ushort)amiiboFile.LastWriteDate.Year,
+ LastWriteMonth = (byte)amiiboFile.LastWriteDate.Month,
+ LastWriteDay = (byte)amiiboFile.LastWriteDate.Day,
+ WriteCounter = amiiboFile.WriteCounter,
+ Version = 1,
+ ApplicationAreaSize = AmiiboConstants.ApplicationAreaSize,
+ Reserved = new Array52<byte>()
+ };
+ }
+
+ public static RegisterInfo GetRegisterInfo(string amiiboId)
+ {
+ VirtualAmiiboFile amiiboFile = LoadAmiiboFile(amiiboId);
+
+ UtilityImpl utilityImpl = new UtilityImpl();
+ CharInfo charInfo = new CharInfo();
+
+ charInfo.SetFromStoreData(StoreData.BuildDefault(utilityImpl, 0));
+
+ // TODO: Maybe change the "no name" by the player name when user profile will be implemented.
+ // charInfo.Nickname = Nickname.FromString("Nickname");
+
+ RegisterInfo registerInfo = new RegisterInfo()
+ {
+ MiiCharInfo = charInfo,
+ FirstWriteYear = (ushort)amiiboFile.FirstWriteDate.Year,
+ FirstWriteMonth = (byte)amiiboFile.FirstWriteDate.Month,
+ FirstWriteDay = (byte)amiiboFile.FirstWriteDate.Day,
+ FontRegion = 0,
+ Reserved1 = new Array64<byte>(),
+ Reserved2 = new Array58<byte>()
+ };
+
+ Encoding.ASCII.GetBytes("Ryujinx").CopyTo(registerInfo.Nickname.ToSpan());
+
+ return registerInfo;
+ }
+
+ public static bool OpenApplicationArea(string amiiboId, uint applicationAreaId)
+ {
+ VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
+
+ if (virtualAmiiboFile.ApplicationAreas.Any(item => item.ApplicationAreaId == applicationAreaId))
+ {
+ _openedApplicationAreaId = applicationAreaId;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public static byte[] GetApplicationArea(string amiiboId)
+ {
+ VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
+
+ foreach (VirtualAmiiboApplicationArea applicationArea in virtualAmiiboFile.ApplicationAreas)
+ {
+ if (applicationArea.ApplicationAreaId == _openedApplicationAreaId)
+ {
+ return applicationArea.ApplicationArea;
+ }
+ }
+
+ return Array.Empty<byte>();
+ }
+
+ public static bool CreateApplicationArea(string amiiboId, uint applicationAreaId, byte[] applicationAreaData)
+ {
+ VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
+
+ if (virtualAmiiboFile.ApplicationAreas.Any(item => item.ApplicationAreaId == applicationAreaId))
+ {
+ return false;
+ }
+
+ virtualAmiiboFile.ApplicationAreas.Add(new VirtualAmiiboApplicationArea()
+ {
+ ApplicationAreaId = applicationAreaId,
+ ApplicationArea = applicationAreaData
+ });
+
+ SaveAmiiboFile(virtualAmiiboFile);
+
+ return true;
+ }
+
+ public static void SetApplicationArea(string amiiboId, byte[] applicationAreaData)
+ {
+ VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
+
+ if (virtualAmiiboFile.ApplicationAreas.Any(item => item.ApplicationAreaId == _openedApplicationAreaId))
+ {
+ for (int i = 0; i < virtualAmiiboFile.ApplicationAreas.Count; i++)
+ {
+ if (virtualAmiiboFile.ApplicationAreas[i].ApplicationAreaId == _openedApplicationAreaId)
+ {
+ virtualAmiiboFile.ApplicationAreas[i] = new VirtualAmiiboApplicationArea()
+ {
+ ApplicationAreaId = _openedApplicationAreaId,
+ ApplicationArea = applicationAreaData
+ };
+
+ break;
+ }
+ }
+
+ SaveAmiiboFile(virtualAmiiboFile);
+ }
+ }
+
+ private static VirtualAmiiboFile LoadAmiiboFile(string amiiboId)
+ {
+ Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
+
+ string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{amiiboId}.json");
+
+ VirtualAmiiboFile virtualAmiiboFile;
+
+ if (File.Exists(filePath))
+ {
+ virtualAmiiboFile = JsonSerializer.Deserialize<VirtualAmiiboFile>(File.ReadAllText(filePath));
+ }
+ else
+ {
+ virtualAmiiboFile = new VirtualAmiiboFile()
+ {
+ FileVersion = 0,
+ TagUuid = Array.Empty<byte>(),
+ AmiiboId = amiiboId,
+ FirstWriteDate = DateTime.Now,
+ LastWriteDate = DateTime.Now,
+ WriteCounter = 0,
+ ApplicationAreas = new List<VirtualAmiiboApplicationArea>()
+ };
+
+ SaveAmiiboFile(virtualAmiiboFile);
+ }
+
+ return virtualAmiiboFile;
+ }
+
+ private static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile)
+ {
+ string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json");
+
+ File.WriteAllText(filePath, JsonSerializer.Serialize(virtualAmiiboFile));
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj
index 5dd250f4..4a5d7508 100644
--- a/Ryujinx/Ryujinx.csproj
+++ b/Ryujinx/Ryujinx.csproj
@@ -70,6 +70,7 @@
<None Remove="Ui\Resources\Icon_NSO.png" />
<None Remove="Ui\Resources\Icon_NSP.png" />
<None Remove="Ui\Resources\Icon_XCI.png" />
+ <None Remove="Ui\Resources\Logo_Amiibo.png" />
<None Remove="Ui\Resources\Logo_Discord.png" />
<None Remove="Ui\Resources\Logo_GitHub.png" />
<None Remove="Ui\Resources\Logo_Patreon.png" />
@@ -94,6 +95,7 @@
<EmbeddedResource Include="Ui\Resources\Icon_NSO.png" />
<EmbeddedResource Include="Ui\Resources\Icon_NSP.png" />
<EmbeddedResource Include="Ui\Resources\Icon_XCI.png" />
+ <EmbeddedResource Include="Ui\Resources\Logo_Amiibo.png" />
<EmbeddedResource Include="Ui\Resources\Logo_Discord.png" />
<EmbeddedResource Include="Ui\Resources\Logo_GitHub.png" />
<EmbeddedResource Include="Ui\Resources\Logo_Patreon.png" />
diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs
index 7d48422c..634a1781 100644
--- a/Ryujinx/Ui/MainWindow.cs
+++ b/Ryujinx/Ui/MainWindow.cs
@@ -57,6 +57,9 @@ namespace Ryujinx.Ui
private string _currentEmulatedGamePath = null;
+ private string _lastScannedAmiiboId = "";
+ private bool _lastScannedAmiiboShowAll = false;
+
public GlRenderer GlRendererWidget;
#pragma warning disable CS0169, CS0649, IDE0044
@@ -66,8 +69,11 @@ namespace Ryujinx.Ui
[GUI] MenuBar _menuBar;
[GUI] Box _footerBox;
[GUI] Box _statusBar;
+ [GUI] MenuItem _optionMenu;
+ [GUI] MenuItem _actionMenu;
[GUI] MenuItem _stopEmulation;
[GUI] MenuItem _simulateWakeUpMessage;
+ [GUI] MenuItem _scanAmiibo;
[GUI] MenuItem _fullScreen;
[GUI] CheckMenuItem _startFullScreen;
[GUI] CheckMenuItem _favToggle;
@@ -141,6 +147,8 @@ namespace Ryujinx.Ui
_applicationLibrary.ApplicationAdded += Application_Added;
_applicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated;
+ _actionMenu.StateChanged += ActionMenu_StateChanged;
+
_gameTable.ButtonReleaseEvent += Row_Clicked;
_fullScreen.Activated += FullScreen_Toggled;
@@ -151,8 +159,7 @@ namespace Ryujinx.Ui
_startFullScreen.Active = true;
}
- _stopEmulation.Sensitive = false;
- _simulateWakeUpMessage.Sensitive = false;
+ _actionMenu.Sensitive = false;
if (ConfigurationState.Instance.Ui.GuiColumns.FavColumn) _favToggle.Active = true;
if (ConfigurationState.Instance.Ui.GuiColumns.IconColumn) _iconToggle.Active = true;
@@ -594,9 +601,10 @@ namespace Ryujinx.Ui
windowThread.Start();
#endif
- _gameLoaded = true;
- _stopEmulation.Sensitive = true;
- _simulateWakeUpMessage.Sensitive = true;
+ _gameLoaded = true;
+ _actionMenu.Sensitive = true;
+
+ _lastScannedAmiiboId = "";
_firmwareInstallFile.Sensitive = false;
_firmwareInstallDirectory.Sensitive = false;
@@ -692,8 +700,7 @@ namespace Ryujinx.Ui
Task.Run(RefreshFirmwareLabel);
Task.Run(HandleRelaunch);
- _stopEmulation.Sensitive = false;
- _simulateWakeUpMessage.Sensitive = false;
+ _actionMenu.Sensitive = false;
_firmwareInstallFile.Sensitive = true;
_firmwareInstallDirectory.Sensitive = true;
});
@@ -1179,6 +1186,44 @@ namespace Ryujinx.Ui
}
}
+ private void ActionMenu_StateChanged(object o, StateChangedArgs args)
+ {
+ _scanAmiibo.Sensitive = _emulationContext != null && _emulationContext.System.SearchingForAmiibo(out int _);
+ }
+
+ private void Scan_Amiibo(object sender, EventArgs args)
+ {
+ if (_emulationContext.System.SearchingForAmiibo(out int deviceId))
+ {
+ AmiiboWindow amiiboWindow = new AmiiboWindow
+ {
+ LastScannedAmiiboShowAll = _lastScannedAmiiboShowAll,
+ LastScannedAmiiboId = _lastScannedAmiiboId,
+ DeviceId = deviceId,
+ TitleId = _emulationContext.Application.TitleIdText.ToUpper()
+ };
+
+ amiiboWindow.DeleteEvent += AmiiboWindow_DeleteEvent;
+
+ amiiboWindow.Show();
+ }
+ else
+ {
+ GtkDialog.CreateInfoDialog($"Amiibo", "The game is currently not ready to receive Amiibo scan data. Ensure that you have an Amiibo-compatible game open and ready to receive Amiibo scan data.");
+ }
+ }
+
+ private void AmiiboWindow_DeleteEvent(object sender, DeleteEventArgs args)
+ {
+ if (((AmiiboWindow)sender).AmiiboId != "" && ((AmiiboWindow)sender).Response == ResponseType.Ok)
+ {
+ _lastScannedAmiiboId = ((AmiiboWindow)sender).AmiiboId;
+ _lastScannedAmiiboShowAll = ((AmiiboWindow)sender).LastScannedAmiiboShowAll;
+
+ _emulationContext.System.ScanAmiibo(((AmiiboWindow)sender).DeviceId, ((AmiiboWindow)sender).AmiiboId, ((AmiiboWindow)sender).UseRandomUuid);
+ }
+ }
+
private void Update_Pressed(object sender, EventArgs args)
{
if (Updater.CanUpdate(true))
diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade
index 5558403b..beeed265 100644
--- a/Ryujinx/Ui/MainWindow.glade
+++ b/Ryujinx/Ui/MainWindow.glade
@@ -95,7 +95,7 @@
</object>
</child>
<child>
- <object class="GtkMenuItem" id="OptionsMenu">
+ <object class="GtkMenuItem" id="_optionMenu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Options</property>
@@ -128,32 +128,6 @@
</object>
</child>
<child>
- <object class="GtkMenuItem" id="_stopEmulation">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="tooltip_text" translatable="yes">Stop emulation of the current game and return to game selection</property>
- <property name="label" translatable="yes">Stop Emulation</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="StopEmulation_Pressed" swapped="no"/>
- </object>
- </child>
- <child>
- <object class="GtkMenuItem" id="_simulateWakeUpMessage">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="tooltip_text" translatable="yes">Simulate a Wake-up Message</property>
- <property name="label" translatable="yes">Simulate Wake-up Message</property>
- <property name="use_underline">True</property>
- <signal name="activate" handler="Simulate_WakeUp_Message_Pressed" swapped="no"/>
- </object>
- </child>
- <child>
- <object class="GtkSeparatorMenuItem">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- </object>
- </child>
- <child>
<object class="GtkMenuItem" id="GUIColumns">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -279,6 +253,56 @@
</object>
</child>
<child>
+ <object class="GtkMenuItem" id="_actionMenu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Actions</property>
+ <property name="use_underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="_stopEmulation">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Stop emulation of the current game and return to game selection</property>
+ <property name="label" translatable="yes">Stop Emulation</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="StopEmulation_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_simulateWakeUpMessage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Simulate a Wake-up Message</property>
+ <property name="label" translatable="yes">Simulate Wake-up Message</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="Simulate_WakeUp_Message_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_scanAmiibo">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">Scan an Amiibo</property>
+ <property name="label" translatable="yes">Scan an Amiibo</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="Scan_Amiibo" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
<object class="GtkMenuItem" id="_toolsMenu">
<property name="visible">True</property>
<property name="can_focus">False</property>
diff --git a/Ryujinx/Ui/Resources/Logo_Amiibo.png b/Ryujinx/Ui/Resources/Logo_Amiibo.png
new file mode 100644
index 00000000..05e7c944
--- /dev/null
+++ b/Ryujinx/Ui/Resources/Logo_Amiibo.png
Binary files differ
diff --git a/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs b/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs
new file mode 100644
index 00000000..3480c6e8
--- /dev/null
+++ b/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs
@@ -0,0 +1,194 @@
+using Gtk;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class AmiiboWindow : Window
+ {
+ private Box _mainBox;
+ private ButtonBox _buttonBox;
+ private Button _scanButton;
+ private Button _cancelButton;
+ private CheckButton _randomUuidCheckBox;
+ private Box _amiiboBox;
+ private Box _amiiboHeadBox;
+ private Box _amiiboSeriesBox;
+ private Label _amiiboSeriesLabel;
+ private ComboBoxText _amiiboSeriesComboBox;
+ private Box _amiiboCharsBox;
+ private Label _amiiboCharsLabel;
+ private ComboBoxText _amiiboCharsComboBox;
+ private CheckButton _showAllCheckBox;
+ private Image _amiiboImage;
+ private Label _gameUsageLabel;
+
+ private void InitializeComponent()
+ {
+#pragma warning disable CS0612
+
+ //
+ // AmiiboWindow
+ //
+ CanFocus = false;
+ Resizable = false;
+ Modal = true;
+ WindowPosition = WindowPosition.Center;
+ DefaultWidth = 600;
+ DefaultHeight = 470;
+ TypeHint = Gdk.WindowTypeHint.Dialog;
+
+ //
+ // _mainBox
+ //
+ _mainBox = new Box(Orientation.Vertical, 2);
+
+ //
+ // _buttonBox
+ //
+ _buttonBox = new ButtonBox(Orientation.Horizontal)
+ {
+ Margin = 20,
+ LayoutStyle = ButtonBoxStyle.End
+ };
+
+ //
+ // _scanButton
+ //
+ _scanButton = new Button()
+ {
+ Label = "Scan It!",
+ CanFocus = true,
+ ReceivesDefault = true,
+ MarginLeft = 10
+ };
+ _scanButton.Clicked += ScanButton_Pressed;
+
+ //
+ // _randomUuidCheckBox
+ //
+ _randomUuidCheckBox = new CheckButton()
+ {
+ Label = "Hack: Use Random Tag Uuid",
+ TooltipText = "This allows multiple scans of a single Amiibo.\n(used in The Legend of Zelda: Breath of the Wild)"
+ };
+
+ //
+ // _cancelButton
+ //
+ _cancelButton = new Button()
+ {
+ Label = "Cancel",
+ CanFocus = true,
+ ReceivesDefault = true,
+ MarginLeft = 10
+ };
+ _cancelButton.Clicked += CancelButton_Pressed;
+
+ //
+ // _amiiboBox
+ //
+ _amiiboBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _amiiboHeadBox
+ //
+ _amiiboHeadBox = new Box(Orientation.Horizontal, 0)
+ {
+ Margin = 20,
+ Hexpand = true
+ };
+
+ //
+ // _amiiboSeriesBox
+ //
+ _amiiboSeriesBox = new Box(Orientation.Horizontal, 0)
+ {
+ Hexpand = true
+ };
+
+ //
+ // _amiiboSeriesLabel
+ //
+ _amiiboSeriesLabel = new Label("Amiibo Series:");
+
+ //
+ // _amiiboSeriesComboBox
+ //
+ _amiiboSeriesComboBox = new ComboBoxText();
+
+ //
+ // _amiiboCharsBox
+ //
+ _amiiboCharsBox = new Box(Orientation.Horizontal, 0)
+ {
+ Hexpand = true
+ };
+
+ //
+ // _amiiboCharsLabel
+ //
+ _amiiboCharsLabel = new Label("Character:");
+
+ //
+ // _amiiboCharsComboBox
+ //
+ _amiiboCharsComboBox = new ComboBoxText();
+
+ //
+ // _showAllCheckBox
+ //
+ _showAllCheckBox = new CheckButton()
+ {
+ Label = "Show All Amiibo"
+ };
+
+ //
+ // _amiiboImage
+ //
+ _amiiboImage = new Image()
+ {
+ HeightRequest = 350,
+ WidthRequest = 350
+ };
+
+ //
+ // _gameUsageLabel
+ //
+ _gameUsageLabel = new Label("")
+ {
+ MarginTop = 20
+ };
+
+#pragma warning restore CS0612
+
+ ShowComponent();
+ }
+
+ private void ShowComponent()
+ {
+ _buttonBox.Add(_showAllCheckBox);
+ _buttonBox.Add(_randomUuidCheckBox);
+ _buttonBox.Add(_scanButton);
+ _buttonBox.Add(_cancelButton);
+
+ _amiiboSeriesBox.Add(_amiiboSeriesLabel);
+ _amiiboSeriesBox.Add(_amiiboSeriesComboBox);
+
+ _amiiboCharsBox.Add(_amiiboCharsLabel);
+ _amiiboCharsBox.Add(_amiiboCharsComboBox);
+
+ _amiiboHeadBox.Add(_amiiboSeriesBox);
+ _amiiboHeadBox.Add(_amiiboCharsBox);
+
+ _amiiboBox.PackStart(_amiiboHeadBox, true, true, 0);
+ _amiiboBox.PackEnd(_gameUsageLabel, false, false, 0);
+ _amiiboBox.PackEnd(_amiiboImage, false, false, 0);
+
+ _mainBox.Add(_amiiboBox);
+ _mainBox.PackEnd(_buttonBox, false, false, 0);
+
+ Add(_mainBox);
+
+ ShowAll();
+ }
+ }
+} \ No newline at end of file
diff --git a/Ryujinx/Ui/Windows/AmiiboWindow.cs b/Ryujinx/Ui/Windows/AmiiboWindow.cs
new file mode 100644
index 00000000..ac087ce1
--- /dev/null
+++ b/Ryujinx/Ui/Windows/AmiiboWindow.cs
@@ -0,0 +1,422 @@
+using Gtk;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Ui.Widgets;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class AmiiboWindow : Window
+ {
+ private struct AmiiboJson
+ {
+ [JsonPropertyName("amiibo")]
+ public List<AmiiboApi> Amiibo { get; set; }
+ [JsonPropertyName("lastUpdated")]
+ public DateTime LastUpdated { get; set; }
+ }
+
+ private struct AmiiboApi
+ {
+ [JsonPropertyName("name")]
+ public string Name { get; set; }
+ [JsonPropertyName("head")]
+ public string Head { get; set; }
+ [JsonPropertyName("tail")]
+ public string Tail { get; set; }
+ [JsonPropertyName("image")]
+ public string Image { get; set; }
+ [JsonPropertyName("amiiboSeries")]
+ public string AmiiboSeries { get; set; }
+ [JsonPropertyName("character")]
+ public string Character { get; set; }
+ [JsonPropertyName("gameSeries")]
+ public string GameSeries { get; set; }
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+
+ [JsonPropertyName("release")]
+ public Dictionary<string, string> Release { get; set; }
+
+ [JsonPropertyName("gamesSwitch")]
+ public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
+ }
+
+ private class AmiiboApiGamesSwitch
+ {
+ [JsonPropertyName("amiiboUsage")]
+ public List<AmiiboApiUsage> AmiiboUsage { get; set; }
+ [JsonPropertyName("gameID")]
+ public List<string> GameId { get; set; }
+ [JsonPropertyName("gameName")]
+ public string GameName { get; set; }
+ }
+
+ private class AmiiboApiUsage
+ {
+ [JsonPropertyName("Usage")]
+ public string Usage { get; set; }
+ [JsonPropertyName("write")]
+ public bool Write { get; set; }
+ }
+
+ private const string DEFAULT_JSON = "{ \"amiibo\": [] }";
+
+ public string AmiiboId { get; private set; }
+
+ public int DeviceId { get; set; }
+ public string TitleId { get; set; }
+ public string LastScannedAmiiboId { get; set; }
+ public bool LastScannedAmiiboShowAll { get; set; }
+
+ public ResponseType Response { get; private set; }
+
+ public bool UseRandomUuid
+ {
+ get
+ {
+ return _randomUuidCheckBox.Active;
+ }
+ }
+
+ private readonly HttpClient _httpClient;
+ private readonly string _amiiboJsonPath;
+
+ private readonly byte[] _amiiboLogoBytes;
+
+ private List<AmiiboApi> _amiiboList;
+
+ public AmiiboWindow() : base($"Ryujinx {Program.Version} - Amiibo")
+ {
+ Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Ryujinx.png");
+
+ InitializeComponent();
+
+ _httpClient = new HttpClient()
+ {
+ Timeout = TimeSpan.FromMilliseconds(5000)
+ };
+
+ Directory.CreateDirectory(System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
+
+ _amiiboJsonPath = System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
+ _amiiboList = new List<AmiiboApi>();
+
+ _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx/Ui/Resources/Logo_Amiibo.png");
+ _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes);
+
+ _scanButton.Sensitive = false;
+ _randomUuidCheckBox.Sensitive = false;
+
+ _ = LoadContentAsync();
+ }
+
+ private async Task LoadContentAsync()
+ {
+ string amiiboJsonString = DEFAULT_JSON;
+
+ if (File.Exists(_amiiboJsonPath))
+ {
+ amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
+
+ if (await NeedsUpdate(JsonSerializer.Deserialize<AmiiboJson>(amiiboJsonString).LastUpdated))
+ {
+ amiiboJsonString = await DownloadAmiiboJson();
+ }
+ }
+ else
+ {
+ try
+ {
+ amiiboJsonString = await DownloadAmiiboJson();
+ }
+ catch
+ {
+ ShowInfoDialog();
+
+ Close();
+ }
+ }
+
+ _amiiboList = JsonSerializer.Deserialize<AmiiboJson>(amiiboJsonString).Amiibo;
+ _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
+
+ if (LastScannedAmiiboShowAll)
+ {
+ _showAllCheckBox.Click();
+ }
+
+ ParseAmiiboData();
+
+ _showAllCheckBox.Clicked += ShowAllCheckBox_Clicked;
+ }
+
+ private void ParseAmiiboData()
+ {
+ List<string> comboxItemList = new List<string>();
+
+ for (int i = 0; i < _amiiboList.Count; i++)
+ {
+ if (!comboxItemList.Contains(_amiiboList[i].AmiiboSeries))
+ {
+ if (!_showAllCheckBox.Active)
+ {
+ foreach (var game in _amiiboList[i].GamesSwitch)
+ {
+ if (game != null)
+ {
+ if (game.GameId.Contains(TitleId))
+ {
+ comboxItemList.Add(_amiiboList[i].AmiiboSeries);
+ _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries);
+
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ comboxItemList.Add(_amiiboList[i].AmiiboSeries);
+ _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries);
+ }
+ }
+ }
+
+ _amiiboSeriesComboBox.Changed += SeriesComboBox_Changed;
+ _amiiboCharsComboBox.Changed += CharacterComboBox_Changed;
+
+ if (LastScannedAmiiboId != "")
+ {
+ SelectLastScannedAmiibo();
+ }
+ else
+ {
+ _amiiboSeriesComboBox.Active = 0;
+ }
+ }
+
+ private void SelectLastScannedAmiibo()
+ {
+ bool isSet = _amiiboSeriesComboBox.SetActiveId(_amiiboList.FirstOrDefault(amiibo => amiibo.Head + amiibo.Tail == LastScannedAmiiboId).AmiiboSeries);
+ isSet = _amiiboCharsComboBox.SetActiveId(LastScannedAmiiboId);
+
+ if (isSet == false)
+ {
+ _amiiboSeriesComboBox.Active = 0;
+ }
+ }
+
+ private async Task<bool> NeedsUpdate(DateTime oldLastModified)
+ {
+ try
+ {
+ HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://amiibo.ryujinx.org/"));
+
+ if (response.IsSuccessStatusCode)
+ {
+ return response.Content.Headers.LastModified != oldLastModified;
+ }
+
+ return false;
+ }
+ catch
+ {
+ ShowInfoDialog();
+
+ return false;
+ }
+ }
+
+ private async Task<string> DownloadAmiiboJson()
+ {
+ HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/");
+
+ if (response.IsSuccessStatusCode)
+ {
+ string amiiboJsonString = await response.Content.ReadAsStringAsync();
+
+ using (FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
+ {
+ dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
+ }
+
+ return amiiboJsonString;
+ }
+ else
+ {
+ GtkDialog.CreateInfoDialog($"Amiibo API", "An error occured while fetching informations from the API.");
+
+ Close();
+ }
+
+ return DEFAULT_JSON;
+ }
+
+ private async Task UpdateAmiiboPreview(string imageUrl)
+ {
+ HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
+ Gdk.Pixbuf amiiboPreview = new Gdk.Pixbuf(amiiboPreviewBytes);
+
+ float ratio = Math.Min((float)_amiiboImage.AllocatedWidth / amiiboPreview.Width,
+ (float)_amiiboImage.AllocatedHeight / amiiboPreview.Height);
+
+ int resizeHeight = (int)(amiiboPreview.Height * ratio);
+ int resizeWidth = (int)(amiiboPreview.Width * ratio);
+
+ _amiiboImage.Pixbuf = amiiboPreview.ScaleSimple(resizeWidth, resizeHeight, Gdk.InterpType.Bilinear);
+ }
+ }
+
+ private void ShowInfoDialog()
+ {
+ GtkDialog.CreateInfoDialog($"Amiibo API", "Unable to connect to Amiibo API server. The service may be down or you may need to verify your internet connection is online.");
+ }
+
+ //
+ // Events
+ //
+ private void SeriesComboBox_Changed(object sender, EventArgs args)
+ {
+ _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed;
+
+ _amiiboCharsComboBox.RemoveAll();
+
+ List<AmiiboApi> amiiboSortedList = _amiiboList.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeriesComboBox.ActiveId).OrderBy(amiibo => amiibo.Name).ToList();
+
+ List<string> comboxItemList = new List<string>();
+
+ for (int i = 0; i < amiiboSortedList.Count; i++)
+ {
+ if (!comboxItemList.Contains(amiiboSortedList[i].Head + amiiboSortedList[i].Tail))
+ {
+ if (!_showAllCheckBox.Active)
+ {
+ foreach (var game in amiiboSortedList[i].GamesSwitch)
+ {
+ if (game != null)
+ {
+ if (game.GameId.Contains(TitleId))
+ {
+ comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail);
+ _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name);
+
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail);
+ _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name);
+ }
+ }
+ }
+
+ _amiiboCharsComboBox.Changed += CharacterComboBox_Changed;
+
+ _amiiboCharsComboBox.Active = 0;
+
+ _scanButton.Sensitive = true;
+ _randomUuidCheckBox.Sensitive = true;
+ }
+
+ private void CharacterComboBox_Changed(object sender, EventArgs args)
+ {
+ AmiiboId = _amiiboCharsComboBox.ActiveId;
+
+ _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes);
+
+ string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Head + amiibo.Tail == _amiiboCharsComboBox.ActiveId).Image;
+
+ string usageString = "";
+
+ for (int i = 0; i < _amiiboList.Count; i++)
+ {
+ if (_amiiboList[i].Head + _amiiboList[i].Tail == _amiiboCharsComboBox.ActiveId)
+ {
+ bool writable = false;
+
+ foreach (var item in _amiiboList[i].GamesSwitch)
+ {
+ if (item.GameId.Contains(TitleId))
+ {
+ foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
+ {
+ usageString += Environment.NewLine + $"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
+
+ writable = usageItem.Write;
+ }
+ }
+ }
+
+ if (usageString.Length == 0)
+ {
+ usageString = "Unknown.";
+ }
+
+ _gameUsageLabel.Text = $"Usage{(writable ? " (Writable)" : "")} : {usageString}";
+ }
+ }
+
+ _ = UpdateAmiiboPreview(imageUrl);
+ }
+
+ private void ShowAllCheckBox_Clicked(object sender, EventArgs e)
+ {
+ _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes);
+
+ _amiiboSeriesComboBox.Changed -= SeriesComboBox_Changed;
+ _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed;
+
+ _amiiboSeriesComboBox.RemoveAll();
+ _amiiboCharsComboBox.RemoveAll();
+
+ _scanButton.Sensitive = false;
+ _randomUuidCheckBox.Sensitive = false;
+
+ new Task(() => ParseAmiiboData()).Start();
+ }
+
+ private void ScanButton_Pressed(object sender, EventArgs args)
+ {
+ LastScannedAmiiboShowAll = _showAllCheckBox.Active;
+
+ Response = ResponseType.Ok;
+
+ Close();
+ }
+
+ private void CancelButton_Pressed(object sender, EventArgs args)
+ {
+ AmiiboId = "";
+ LastScannedAmiiboId = "";
+ LastScannedAmiiboShowAll = false;
+
+ Response = ResponseType.Cancel;
+
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ _httpClient.Dispose();
+
+ base.Dispose(disposing);
+ }
+ }
+} \ No newline at end of file