Implement Cabinet applet

This implements Nickname support for Amiibos, as well as means of editing the name of an amiibo in games.
This commit is contained in:
yeah-its-gloria 2024-05-08 20:23:59 -04:00
parent a23d8cb92f
commit 02e84fd59a
No known key found for this signature in database
GPG key ID: D9630C1F73389570
8 changed files with 229 additions and 1 deletions

View file

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

View file

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

View file

@ -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<CommonArguments>(normalSession.Pop());
Logger.Info?.PrintMsg(LogClass.ServiceAm, $"CabinetApplet version: 0x{commonArguments.AppletVersion:x8}");
_startArguments = IApplet.ReadStruct<StartParamForAmiiboSettings>(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<byte> 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 });
}
}
}

View file

@ -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<byte> Padding;
public ulong DeviceHandle;
public TagInfo TagInfo;
public RegisterInfo RegisterInfo;
public Array36<byte> Ignored;
}
}

View file

@ -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<byte> StartParamData1;
public TagInfo TagInfo;
public RegisterInfo RegisterInfo;
public Array32<byte> StartParamData2;
public Array36<byte> Unused2;
}
}

View file

@ -0,0 +1,10 @@
namespace Ryujinx.HLE.HOS.Applets
{
enum StartParamForAmiiboSettingsType : byte
{
NicknameAndOwnerSettings = 0,
GameDataEraser = 1,
Restorer = 2,
Formatter = 3,
}
}

View file

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

View file

@ -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<byte>(),
Reserved2 = new Array58<byte>(),
};
"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<byte>(),
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");