diff --git a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs index da4d2e51b..a2ddd573d 100644 --- a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs +++ b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs @@ -1,5 +1,6 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Applets.Browser; +using Ryujinx.HLE.HOS.Applets.Cabinet; using Ryujinx.HLE.HOS.Applets.Dummy; using Ryujinx.HLE.HOS.Applets.Error; using Ryujinx.HLE.HOS.Services.Am.AppletAE; @@ -31,6 +32,8 @@ namespace Ryujinx.HLE.HOS.Applets case AppletId.MiiEdit: Logger.Warning?.Print(LogClass.Application, $"Please use the MiiEdit inside File/Open Applet"); return new DummyApplet(system); + case AppletId.Cabinet: + return new CabinetApplet(system); } Logger.Warning?.Print(LogClass.Application, $"Applet {applet} not implemented!"); 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..07c5dfa49 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs @@ -0,0 +1,199 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using Ryujinx.HLE.HOS.Services.Hid.HidServer; +using Ryujinx.HLE.HOS.Services.Hid; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using LibHac; + +namespace Ryujinx.HLE.HOS.Applets.Cabinet +{ + internal unsafe class CabinetApplet : IApplet + { + private readonly Horizon _system; + private AppletSession _normalSession; + + public event EventHandler AppletStateChanged; + + public CabinetApplet(Horizon system) + { + _system = system; + } + + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + _normalSession = normalSession; + + byte[] launchParams = _normalSession.Pop(); + byte[] startParamBytes = _normalSession.Pop(); + + StartParamForAmiiboSettings startParam = IApplet.ReadStruct(startParamBytes); + + Logger.Stub?.PrintStub(LogClass.ServiceAm, $"CabinetApplet Start Type: {startParam.Type}"); + + switch (startParam.Type) + { + case 0: + StartNicknameAndOwnerSettings(ref startParam); + break; + case 1: + case 3: + StartFormatter(ref startParam); + break; + default: + Logger.Error?.Print(LogClass.ServiceAm, $"Unknown AmiiboSettings type: {startParam.Type}"); + break; + } + + // Prepare the response + ReturnValueForAmiiboSettings returnValue = new() + { + AmiiboSettingsReturnFlag = (byte)AmiiboSettingsReturnFlag.HasRegisterInfo, + DeviceHandle = new DeviceHandle + { + Handle = 0 // Dummy device handle + }, + RegisterInfo = startParam.RegisterInfo + }; + + // Push the response + _normalSession.Push(BuildResponse(returnValue)); + AppletStateChanged?.Invoke(this, null); + + _system.ReturnFocus(); + + return ResultCode.Success; + } + + public ResultCode GetResult() + { + var amiibo = _system.Device.System.NfpDevices[0]; + _system.Device.System.NfpDevices.Remove(amiibo); + return ResultCode.Success; + } + + private void StartFormatter(ref StartParamForAmiiboSettings startParam) + { + // Initialize RegisterInfo + startParam.RegisterInfo = new RegisterInfo(); + } + + private void StartNicknameAndOwnerSettings(ref StartParamForAmiiboSettings startParam) + { + _system.Device.UIHandler.DisplayCabinetDialog(out string newName); + byte[] nameBytes = Encoding.UTF8.GetBytes(newName); + Array41 nickName = new Array41(); + nameBytes.CopyTo(nickName.AsSpan()); + startParam.RegisterInfo.Nickname = nickName; + NfpDevice devicePlayer1 = new() + { + NpadIdType = NpadIdType.Player1, + Handle = HidUtils.GetIndexFromNpadIdType(NpadIdType.Player1), + State = NfpDeviceState.SearchingForTag, + }; + _system.Device.System.NfpDevices.Add(devicePlayer1); + _system.Device.UIHandler.DisplayCabinetMessageDialog(); + string amiiboId = ""; + bool scanned = false; + while (!scanned) + { + for (int i = 0; i < _system.Device.System.NfpDevices.Count; i++) + { + if (_system.Device.System.NfpDevices[i].State == NfpDeviceState.TagFound) + { + amiiboId = _system.Device.System.NfpDevices[i].AmiiboId; + scanned = true; + } + } + } + VirtualAmiibo.UpdateNickName(amiiboId, newName); + } + + private static byte[] BuildResponse(ReturnValueForAmiiboSettings returnValue) + { + int size = Unsafe.SizeOf(); + byte[] bytes = new byte[size]; + + fixed (byte* bytesPtr = bytes) + { + Unsafe.Write(bytesPtr, returnValue); + } + + return bytes; + } + + public static T ReadStruct(byte[] data) where T : unmanaged + { + if (data.Length < Unsafe.SizeOf()) + { + throw new ArgumentException("Not enough data to read the struct"); + } + + fixed (byte* dataPtr = data) + { + return Unsafe.Read(dataPtr); + } + } + + #region Structs + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct TagInfo + { + public fixed byte Data[0x58]; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct StartParamForAmiiboSettings + { + public byte ZeroValue; // Left at zero by sdknso + public byte Type; + public byte Flags; + public byte AmiiboSettingsStartParamOffset28; + public ulong AmiiboSettingsStartParam0; + + public TagInfo TagInfo; // Only enabled when flags bit 1 is set + public RegisterInfo RegisterInfo; // Only enabled when flags bit 2 is set + + public fixed byte StartParamExtraData[0x20]; + + public fixed byte Reserved[0x24]; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct ReturnValueForAmiiboSettings + { + public byte AmiiboSettingsReturnFlag; + private byte Padding1; + private byte Padding2; + private byte Padding3; + public DeviceHandle DeviceHandle; + public TagInfo TagInfo; + public RegisterInfo RegisterInfo; + public fixed byte IgnoredBySdknso[0x24]; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct DeviceHandle + { + public ulong Handle; + } + + public enum AmiiboSettingsReturnFlag : byte + { + Cancel = 0, + HasTagInfo = 2, + HasRegisterInfo = 4, + HasTagInfoAndRegisterInfo = 6 + } + + #endregion + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs index 7ce749d1a..0c685471c 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs @@ -93,6 +93,13 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp return registerInfo; } + public static void UpdateNickName(string amiiboId, string newNickName) + { + VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId); + virtualAmiiboFile.NickName = newNickName; + SaveAmiiboFile(virtualAmiiboFile); + } + public static bool OpenApplicationArea(string amiiboId, uint applicationAreaId) { VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId); diff --git a/src/Ryujinx.HLE/UI/IHostUIHandler.cs b/src/Ryujinx.HLE/UI/IHostUIHandler.cs index 8debfcca0..88af83735 100644 --- a/src/Ryujinx.HLE/UI/IHostUIHandler.cs +++ b/src/Ryujinx.HLE/UI/IHostUIHandler.cs @@ -24,6 +24,18 @@ namespace Ryujinx.HLE.UI /// True when OK is pressed, False otherwise. bool DisplayMessageDialog(ControllerAppletUIArgs args); + /// + /// Displays an Input Dialog box to the user so they can enter the Amiibo's new name + /// + /// Text that the user entered. Set to `null` on internal errors + /// True when OK is pressed, False otherwise. Also returns True on internal errors + bool DisplayCabinetDialog(out string userText); + + /// + /// Displays a Message Dialog box to the user to notify them to scan the Amiibo. + /// + void DisplayCabinetMessageDialog(); + /// /// Tell the UI that we need to transition to another program. /// diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index b7ab8969b..ee0d03171 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -714,6 +714,9 @@ "Never": "Never", "SwkbdMinCharacters": "Must be at least {0} characters long", "SwkbdMinRangeCharacters": "Must be {0}-{1} characters long", + "CabinetTitle": "Cabinet Dialog", + "CabinetDialog": "Enter your Amiibo's new name", + "CabinetScanDialog": "Please scan your Amiibo now.", "SoftwareKeyboard": "Software Keyboard", "SoftwareKeyboardModeNumeric": "Must be 0-9 or '.' only", "SoftwareKeyboardModeAlphabet": "Must be non CJK-characters only", diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs index 2ebba7ac0..3cd68ec66 100644 --- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs +++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs @@ -7,6 +7,7 @@ using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Windows; using Ryujinx.HLE; using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; using Ryujinx.HLE.UI; using Ryujinx.UI.Common.Configuration; @@ -155,6 +156,55 @@ namespace Ryujinx.Ava.UI.Applet return error || okPressed; } + public bool DisplayCabinetDialog(out string userText) + { + ManualResetEvent dialogCloseEvent = new(false); + bool okPressed = false; + string inputText = "My Amiibo"; + Dispatcher.UIThread.InvokeAsync(async () => + { + try + { + _parent.ViewModel.AppHost.NpadManager.BlockInputUpdates(); + SoftwareKeyboardUIArgs args = new SoftwareKeyboardUIArgs(); + args.KeyboardMode = KeyboardMode.Default; + args.InitialText = "Ryujinx"; + args.StringLengthMin = 1; + args.StringLengthMax = 25; + (UserResult result, string userInput) = await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.CabinetDialog], args); + if (result == UserResult.Ok) + { + inputText = userInput; + okPressed = true; + } + } + finally + { + dialogCloseEvent.Set(); + } + }); + dialogCloseEvent.WaitOne(); + _parent.ViewModel.AppHost.NpadManager.UnblockInputUpdates(); + userText = inputText; + return okPressed; + } + + public void DisplayCabinetMessageDialog() + { + ManualResetEvent dialogCloseEvent = new(false); + Dispatcher.UIThread.InvokeAsync(async () => + { + dialogCloseEvent.Set(); + await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.CabinetScanDialog], + "", + LocaleManager.Instance[LocaleKeys.InputDialogOk], + string.Empty, + LocaleManager.Instance[LocaleKeys.CabinetTitle]); + }); + dialogCloseEvent.WaitOne(); + } + + public void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value) { device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);