From 02e84fd59a77654cec1eff3e8e2ec2e791bcd81f Mon Sep 17 00:00:00 2001 From: yeah-its-gloria <32610623+yeah-its-gloria@users.noreply.github.com> Date: Wed, 8 May 2024 20:23:59 -0400 Subject: [PATCH] Implement Cabinet applet This implements Nickname support for Amiibos, as well as means of editing the name of an amiibo in games. --- src/Ryujinx.HLE/HOS/Applets/AppletManager.cs | 1 + .../Cabinet/AmiiboSettingsReturnFlag.cs | 15 +++ .../HOS/Applets/Cabinet/CabinetApplet.cs | 123 ++++++++++++++++++ .../Cabinet/ReturnValueForAmiiboSettings.cs | 17 +++ .../Cabinet/StartParamForAmiiboSettings.cs | 19 +++ .../StartParamForAmiiboSettingsType.cs | 10 ++ .../Nfp/NfpManager/Types/VirtualAmiiboFile.cs | 1 + .../HOS/Services/Nfc/Nfp/VirtualAmiibo.cs | 44 ++++++- 8 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/Ryujinx.HLE/HOS/Applets/Cabinet/AmiiboSettingsReturnFlag.cs create mode 100644 src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs create mode 100644 src/Ryujinx.HLE/HOS/Applets/Cabinet/ReturnValueForAmiiboSettings.cs create mode 100644 src/Ryujinx.HLE/HOS/Applets/Cabinet/StartParamForAmiiboSettings.cs create mode 100644 src/Ryujinx.HLE/HOS/Applets/Cabinet/StartParamForAmiiboSettingsType.cs diff --git a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs index 30300f1b6..77d843318 100644 --- a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs +++ b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs @@ -18,6 +18,7 @@ namespace Ryujinx.HLE.HOS.Applets { AppletId.PlayerSelect, typeof(PlayerSelectApplet) }, { AppletId.Controller, typeof(ControllerApplet) }, { AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) }, + { AppletId.Cabinet, typeof(CabinetApplet) }, { AppletId.LibAppletWeb, typeof(BrowserApplet) }, { AppletId.LibAppletShop, typeof(BrowserApplet) }, { AppletId.LibAppletOff, typeof(BrowserApplet) }, diff --git a/src/Ryujinx.HLE/HOS/Applets/Cabinet/AmiiboSettingsReturnFlag.cs b/src/Ryujinx.HLE/HOS/Applets/Cabinet/AmiiboSettingsReturnFlag.cs new file mode 100644 index 000000000..74b162e9a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Cabinet/AmiiboSettingsReturnFlag.cs @@ -0,0 +1,15 @@ +using System; + +namespace Ryujinx.HLE.HOS.Applets +{ + [Flags] + enum AmiiboSettingsReturnFlag : byte + { + Cancel = 0, + + HasTagInfo = 1 << 1, + HasRegisterInfo = 1 << 2, + + HasCompleteInfo = HasTagInfo | HasRegisterInfo, + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs new file mode 100644 index 000000000..5bd4f4caf --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs @@ -0,0 +1,123 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp; +using System; +using System.IO; +using System.Text; + +namespace Ryujinx.HLE.HOS.Applets +{ + internal class CabinetApplet : IApplet + { + private readonly Horizon _system; + + private AppletSession _normalSession; + private StartParamForAmiiboSettings _startArguments; + + public event EventHandler AppletStateChanged; + + public CabinetApplet(Horizon system) + { + _system = system; + } + + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + _normalSession = normalSession; + + CommonArguments commonArguments = IApplet.ReadStruct(normalSession.Pop()); + + Logger.Info?.PrintMsg(LogClass.ServiceAm, $"CabinetApplet version: 0x{commonArguments.AppletVersion:x8}"); + + _startArguments = IApplet.ReadStruct(normalSession.Pop()); + switch (_startArguments.Type) + { + case StartParamForAmiiboSettingsType.NicknameAndOwnerSettings: + { + // TODO: allow changing the owning Mii + ChangeNickname(); + + break; + } + + default: + throw new NotImplementedException($"CabinetStartType {_startArguments.Type} is not implemented."); + } + + return ResultCode.Success; + } + + public ResultCode GetResult() + { + return ResultCode.Success; + } + + private void ChangeNickname() + { + string nickname = null; + if (_startArguments.Flags.HasFlag(AmiiboSettingsReturnFlag.HasRegisterInfo)) + { + nickname = Encoding.UTF8.GetString(_startArguments.RegisterInfo.Nickname.AsSpan()).TrimEnd('\0'); + } + + SoftwareKeyboardUIArgs inputParameters = new() + { + HeaderText = "Enter a new nickname for this amiibo.", + GuideText = nickname, + StringLengthMin = 1, + StringLengthMax = 10, + }; + + bool inputResult = _system.Device.UIHandler.DisplayInputDialog(inputParameters, out string newNickname); + if (!inputResult) + { + ReturnCancel(); + + return; + } + + VirtualAmiibo.SetNickname(_startArguments.TagInfo.Uuid.AsSpan()[..9].ToArray(), newNickname); + + ReturnValueForAmiiboSettings returnValue = new() + { + Flags = AmiiboSettingsReturnFlag.HasCompleteInfo, + DeviceHandle = (ulong)_system.NfpDevices[0].Handle, + TagInfo = _startArguments.TagInfo, + RegisterInfo = _startArguments.RegisterInfo, + }; + + Span nicknameData = returnValue.RegisterInfo.Nickname.AsSpan(); + nicknameData.Clear(); + + Encoding.UTF8.GetBytes(newNickname).CopyTo(nicknameData); + + _normalSession.Push(BuildResponse(returnValue)); + AppletStateChanged?.Invoke(this, null); + _system.ReturnFocus(); + } + + private void ReturnCancel() + { + _normalSession.Push(BuildResponse()); + AppletStateChanged?.Invoke(this, null); + _system.ReturnFocus(); + } + + private static byte[] BuildResponse(ReturnValueForAmiiboSettings result) + { + using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); + using BinaryWriter writer = new(stream); + + writer.WriteStruct(result); + + return stream.ToArray(); + } + + private static byte[] BuildResponse() + { + return BuildResponse(new ReturnValueForAmiiboSettings { Flags = AmiiboSettingsReturnFlag.Cancel }); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Cabinet/ReturnValueForAmiiboSettings.cs b/src/Ryujinx.HLE/HOS/Applets/Cabinet/ReturnValueForAmiiboSettings.cs new file mode 100644 index 000000000..bc50b287f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Cabinet/ReturnValueForAmiiboSettings.cs @@ -0,0 +1,17 @@ +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets +{ + [StructLayout(LayoutKind.Sequential, Size = 0x188, Pack = 1)] + struct ReturnValueForAmiiboSettings + { + public AmiiboSettingsReturnFlag Flags; + public Array3 Padding; + public ulong DeviceHandle; + public TagInfo TagInfo; + public RegisterInfo RegisterInfo; + public Array36 Ignored; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Cabinet/StartParamForAmiiboSettings.cs b/src/Ryujinx.HLE/HOS/Applets/Cabinet/StartParamForAmiiboSettings.cs new file mode 100644 index 000000000..b85fc6ec5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Cabinet/StartParamForAmiiboSettings.cs @@ -0,0 +1,19 @@ +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets +{ + [StructLayout(LayoutKind.Sequential, Size = 0x1a8)] + struct StartParamForAmiiboSettings + { + public byte Unused1; + public StartParamForAmiiboSettingsType Type; + public AmiiboSettingsReturnFlag Flags; + public Array9 StartParamData1; + public TagInfo TagInfo; + public RegisterInfo RegisterInfo; + public Array32 StartParamData2; + public Array36 Unused2; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Cabinet/StartParamForAmiiboSettingsType.cs b/src/Ryujinx.HLE/HOS/Applets/Cabinet/StartParamForAmiiboSettingsType.cs new file mode 100644 index 000000000..c48cbe753 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Cabinet/StartParamForAmiiboSettingsType.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Applets +{ + enum StartParamForAmiiboSettingsType : byte + { + NicknameAndOwnerSettings = 0, + GameDataEraser = 1, + Restorer = 2, + Formatter = 3, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs index 65d380979..4557e8954 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs @@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager struct VirtualAmiiboFile { public uint FileVersion { get; set; } + public string Nickname { get; set; } public byte[] TagUuid { get; set; } public string AmiiboId { get; set; } public DateTime FirstWriteDate { get; set; } diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs index ba4a81e0e..d57222ab0 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs @@ -8,6 +8,8 @@ using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text; namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp { @@ -85,7 +87,8 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp Reserved1 = new Array64(), Reserved2 = new Array58(), }; - "Ryujinx"u8.CopyTo(registerInfo.Nickname.AsSpan()); + + Encoding.UTF8.GetBytes(amiiboFile.Nickname).CopyTo(registerInfo.Nickname.AsSpan()); return registerInfo; } @@ -163,6 +166,15 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp } } + public static void SetNickname(byte[] tagUuid, string nickname) + { + VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(tagUuid); + + virtualAmiiboFile.Nickname = nickname; + + SaveAmiiboFile(virtualAmiiboFile); + } + private static VirtualAmiiboFile LoadAmiiboFile(string amiiboId) { Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo")); @@ -174,12 +186,20 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp if (File.Exists(filePath)) { virtualAmiiboFile = JsonHelper.DeserializeFromFile(filePath, _serializerContext.VirtualAmiiboFile); + + if (virtualAmiiboFile.Nickname == null) + { + virtualAmiiboFile.Nickname = "Ryujinx"; + + SaveAmiiboFile(virtualAmiiboFile); + } } else { virtualAmiiboFile = new VirtualAmiiboFile() { FileVersion = 0, + Nickname = "Ryujinx", TagUuid = Array.Empty(), AmiiboId = amiiboId, FirstWriteDate = DateTime.Now, @@ -194,6 +214,28 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp return virtualAmiiboFile; } + private static VirtualAmiiboFile LoadAmiiboFile(byte[] tagUuid) + { + VirtualAmiiboFile virtualAmiiboFile; + + string[] paths = Directory.GetFiles(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"), "*.json"); + foreach (string path in paths) + { + if (path.EndsWith("Amiibo.json")) + { + continue; + } + + virtualAmiiboFile = JsonHelper.DeserializeFromFile(path, _serializerContext.VirtualAmiiboFile); + if (virtualAmiiboFile.TagUuid.SequenceEqual(tagUuid)) + { + return virtualAmiiboFile; + } + } + + throw new FileNotFoundException(); + } + private static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile) { string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json");