nfp: Amiibo scanning support (#2006)

* Initial Impl.

* You just want me cause I'm next

* Fix some logics

* Fix close button
This commit is contained in:
Ac_K 2021-03-18 21:40:20 +01:00 committed by GitHub
parent 2b92c10105
commit a56423802c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1830 additions and 144 deletions

View File

@ -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();

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
{
enum DeviceType : uint
{
Amiibo
}
}

View File

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

View File

@ -0,0 +1,9 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
{
enum MountTarget : uint
{
Rom = 1,
Ram = 2,
All = 3
}
}

View File

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

View File

@ -1,6 +1,6 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.UserManager
{
enum DeviceState
enum NfpDeviceState
{
Initialized = 0,
SearchingForTag = 1,

View File

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

View File

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

View File

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

View File

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

View File

@ -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" />

View File

@ -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))

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

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