From a56423802cd6e74809c1cf5d93b51fdf11f07bef Mon Sep 17 00:00:00 2001 From: Ac_K Date: Thu, 18 Mar 2021 21:40:20 +0100 Subject: [PATCH] nfp: Amiibo scanning support (#2006) * Initial Impl. * You just want me cause I'm next * Fix some logics * Fix close button --- Ryujinx.HLE/HOS/Horizon.cs | 33 + .../HOS/Services/Mii/Types/CoreData.cs | 14 +- .../HOS/Services/Nfc/Nfp/ResultCode.cs | 9 +- .../HOS/Services/Nfc/Nfp/UserManager/IUser.cs | 800 ++++++++++++++++-- .../Nfp/UserManager/Types/AmiiboConstants.cs | 8 + .../Nfc/Nfp/UserManager/Types/CommonInfo.cs | 17 + .../Nfc/Nfp/UserManager/Types/Device.cs | 19 - .../Nfc/Nfp/UserManager/Types/DeviceType.cs | 7 + .../Nfc/Nfp/UserManager/Types/ModelInfo.cs | 16 + .../Nfc/Nfp/UserManager/Types/MountTarget.cs | 9 + .../Nfc/Nfp/UserManager/Types/NfpDevice.cs | 23 + .../{DeviceState.cs => NfpDeviceState.cs} | 2 +- .../Nfc/Nfp/UserManager/Types/RegisterInfo.cs | 19 + .../Nfc/Nfp/UserManager/Types/TagInfo.cs | 16 + .../UserManager/Types/VirtualAmiiboFile.cs | 22 + .../HOS/Services/Nfc/Nfp/VirtualAmiibo.cs | 205 +++++ Ryujinx/Ryujinx.csproj | 2 + Ryujinx/Ui/MainWindow.cs | 59 +- Ryujinx/Ui/MainWindow.glade | 78 +- Ryujinx/Ui/Resources/Logo_Amiibo.png | Bin 0 -> 11676 bytes Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs | 194 +++++ Ryujinx/Ui/Windows/AmiiboWindow.cs | 422 +++++++++ 22 files changed, 1830 insertions(+), 144 deletions(-) create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/AmiiboConstants.cs create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/CommonInfo.cs delete mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/Device.cs create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceType.cs create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/ModelInfo.cs create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/MountTarget.cs create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/NfpDevice.cs rename Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/{DeviceState.cs => NfpDeviceState.cs} (91%) create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/RegisterInfo.cs create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/TagInfo.cs create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/VirtualAmiiboFile.cs create mode 100644 Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs create mode 100644 Ryujinx/Ui/Resources/Logo_Amiibo.png create mode 100644 Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs create mode 100644 Ryujinx/Ui/Windows/AmiiboWindow.cs diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index 16b4c3760..4da147bf5 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 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(); + // 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 1b11f99de..39a3945b0 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 b42a28a91..e0ccbc6d6 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 2cd35b9ed..908815656 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 _devices = new List(); + private CancellationTokenSource _cancelTokenSource; public IUser() { } @@ -25,32 +34,30 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp // Initialize(u64, u64, pid, buffer) 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]; + _mcuData = new byte[inputSize]; - context.Memory.Read((ulong)inputPosition, unknownBuffer); + context.Memory.Read((ulong)inputPosition, _mcuData); - // 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. + // TODO: The mcuData buffer seems to contains entries with a size of 0x40 bytes each. Usage of the data needs to be determined. - // 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: 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++) - { - context.Memory.Write((ulong)(outputPosition + (i * sizeof(long))), (uint)_devices[i].Handle); - } + MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize); - context.ResponseData.Write(_devices.Count); + if (CheckNfcIsEnabled() == ResultCode.Success) + { + 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(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) 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) 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) 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, 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(), + Protocol = uint.MaxValue, // All Protocol + TagType = uint.MaxValue, // All Type + Reserved2 = new Array6() + }; + + 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, 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, 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, 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() + }; + + 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 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 000000000..47f6f0fa7 --- /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 000000000..da055dc36 --- /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 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 3ff3489bb..000000000 --- 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 000000000..753b91a93 --- /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 000000000..1b6a3d32a --- /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 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 000000000..11520bc6d --- /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 000000000..b0d9c8060 --- /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 similarity index 91% rename from Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceState.cs rename to Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/NfpDeviceState.cs index 7e3734941..0e7532507 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 000000000..3c72a9715 --- /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 Nickname; + public byte FontRegion; + public Array64 Reserved1; + public Array58 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 000000000..950f8c104 --- /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 Uuid; + public byte UuidLength; + public Array21 Reserved1; + public uint Protocol; + public uint TagType; + public Array6 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 000000000..5265c0389 --- /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 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 000000000..bd810d96a --- /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() + }; + } + + 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(), + Reserved2 = new Array58() + }; + + 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(); + } + + 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(File.ReadAllText(filePath)); + } + else + { + virtualAmiiboFile = new VirtualAmiiboFile() + { + FileVersion = 0, + TagUuid = Array.Empty(), + AmiiboId = amiiboId, + FirstWriteDate = DateTime.Now, + LastWriteDate = DateTime.Now, + WriteCounter = 0, + ApplicationAreas = new List() + }; + + 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 5dd250f4a..4a5d75087 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -70,6 +70,7 @@ + @@ -94,6 +95,7 @@ + diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 7d48422c1..634a17816 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 5558403ba..beeed265a 100644 --- a/Ryujinx/Ui/MainWindow.glade +++ b/Ryujinx/Ui/MainWindow.glade @@ -95,7 +95,7 @@ - + True False Options @@ -127,32 +127,6 @@ False - - - True - False - Stop emulation of the current game and return to game selection - Stop Emulation - True - - - - - - True - False - Simulate a Wake-up Message - Simulate Wake-up Message - True - - - - - - True - False - - True @@ -278,6 +252,56 @@ + + + True + False + Actions + True + + + True + False + + + True + False + Stop emulation of the current game and return to game selection + Stop Emulation + True + + + + + + True + False + + + + + True + False + Simulate a Wake-up Message + Simulate Wake-up Message + True + + + + + + True + False + Scan an Amiibo + Scan an Amiibo + True + + + + + + + True diff --git a/Ryujinx/Ui/Resources/Logo_Amiibo.png b/Ryujinx/Ui/Resources/Logo_Amiibo.png new file mode 100644 index 0000000000000000000000000000000000000000..05e7c944ea43850a9eefa1ff1185abf4da01a031 GIT binary patch literal 11676 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 HB{Ts5PqDxQ literal 0 HcmV?d00001 diff --git a/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs b/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs new file mode 100644 index 000000000..3480c6e8a --- /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 000000000..ac087ce12 --- /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 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 Release { get; set; } + + [JsonPropertyName("gamesSwitch")] + public List GamesSwitch { get; set; } + } + + private class AmiiboApiGamesSwitch + { + [JsonPropertyName("amiiboUsage")] + public List AmiiboUsage { get; set; } + [JsonPropertyName("gameID")] + public List 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 _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(); + + _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(amiiboJsonString).LastUpdated)) + { + amiiboJsonString = await DownloadAmiiboJson(); + } + } + else + { + try + { + amiiboJsonString = await DownloadAmiiboJson(); + } + catch + { + ShowInfoDialog(); + + Close(); + } + } + + _amiiboList = JsonSerializer.Deserialize(amiiboJsonString).Amiibo; + _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList(); + + if (LastScannedAmiiboShowAll) + { + _showAllCheckBox.Click(); + } + + ParseAmiiboData(); + + _showAllCheckBox.Clicked += ShowAllCheckBox_Clicked; + } + + private void ParseAmiiboData() + { + List comboxItemList = new List(); + + 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 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 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 amiiboSortedList = _amiiboList.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeriesComboBox.ActiveId).OrderBy(amiibo => amiibo.Name).ToList(); + + List comboxItemList = new List(); + + 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