Răsfoiți Sursa

nfp: Amiibo scanning support (#2006)

* Initial Impl.

* You just want me cause I'm next

* Fix some logics

* Fix close button
Ac_K 5 ani în urmă
părinte
comite
a56423802c

+ 33 - 0
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();

+ 7 - 7
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);
 

+ 7 - 2
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
     }
 }

Fișier diff suprimat deoarece este prea mare
+ 712 - 80
Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/IUser.cs


+ 8 - 0
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;
+    }
+}

+ 17 - 0
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;
+    }
+}

+ 0 - 19
Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/Device.cs

@@ -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;
-    }
-}

+ 7 - 0
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
+    }
+}

+ 16 - 0
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;
+    }
+}

+ 9 - 0
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
+    }
+}

+ 23 - 0
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;
+    }
+}

+ 1 - 1
Ryujinx.HLE/HOS/Services/Nfc/Nfp/UserManager/Types/DeviceState.cs → 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,

+ 19 - 0
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;
+    }
+}

+ 16 - 0
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;
+    }
+}

+ 22 - 0
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; }
+    }
+}

+ 205 - 0
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));
+        }
+    }
+}

+ 2 - 0
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" />

+ 52 - 7
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))

+ 51 - 27
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>
@@ -127,32 +127,6 @@
                         <property name="can_focus">False</property>
                       </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>
@@ -278,6 +252,56 @@
                 </child>
               </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>

BIN
Ryujinx/Ui/Resources/Logo_Amiibo.png


+ 194 - 0
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();
+        }
+    }
+}

+ 422 - 0
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);
+        }
+    }
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff