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:
parent
a23d8cb92f
commit
02e84fd59a
8 changed files with 229 additions and 1 deletions
|
@ -18,6 +18,7 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||||
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) },
|
{ AppletId.PlayerSelect, typeof(PlayerSelectApplet) },
|
||||||
{ AppletId.Controller, typeof(ControllerApplet) },
|
{ AppletId.Controller, typeof(ControllerApplet) },
|
||||||
{ AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) },
|
{ AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) },
|
||||||
|
{ AppletId.Cabinet, typeof(CabinetApplet) },
|
||||||
{ AppletId.LibAppletWeb, typeof(BrowserApplet) },
|
{ AppletId.LibAppletWeb, typeof(BrowserApplet) },
|
||||||
{ AppletId.LibAppletShop, typeof(BrowserApplet) },
|
{ AppletId.LibAppletShop, typeof(BrowserApplet) },
|
||||||
{ AppletId.LibAppletOff, typeof(BrowserApplet) },
|
{ AppletId.LibAppletOff, typeof(BrowserApplet) },
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
123
src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs
Normal file
123
src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Ryujinx.HLE.HOS.Applets
|
||||||
|
{
|
||||||
|
enum StartParamForAmiiboSettingsType : byte
|
||||||
|
{
|
||||||
|
NicknameAndOwnerSettings = 0,
|
||||||
|
GameDataEraser = 1,
|
||||||
|
Restorer = 2,
|
||||||
|
Formatter = 3,
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager
|
||||||
struct VirtualAmiiboFile
|
struct VirtualAmiiboFile
|
||||||
{
|
{
|
||||||
public uint FileVersion { get; set; }
|
public uint FileVersion { get; set; }
|
||||||
|
public string Nickname { get; set; }
|
||||||
public byte[] TagUuid { get; set; }
|
public byte[] TagUuid { get; set; }
|
||||||
public string AmiiboId { get; set; }
|
public string AmiiboId { get; set; }
|
||||||
public DateTime FirstWriteDate { get; set; }
|
public DateTime FirstWriteDate { get; set; }
|
||||||
|
|
|
@ -8,6 +8,8 @@ using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
||||||
{
|
{
|
||||||
|
@ -85,7 +87,8 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
||||||
Reserved1 = new Array64<byte>(),
|
Reserved1 = new Array64<byte>(),
|
||||||
Reserved2 = new Array58<byte>(),
|
Reserved2 = new Array58<byte>(),
|
||||||
};
|
};
|
||||||
"Ryujinx"u8.CopyTo(registerInfo.Nickname.AsSpan());
|
|
||||||
|
Encoding.UTF8.GetBytes(amiiboFile.Nickname).CopyTo(registerInfo.Nickname.AsSpan());
|
||||||
|
|
||||||
return registerInfo;
|
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)
|
private static VirtualAmiiboFile LoadAmiiboFile(string amiiboId)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
|
Directory.CreateDirectory(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
|
||||||
|
@ -174,12 +186,20 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
{
|
{
|
||||||
virtualAmiiboFile = JsonHelper.DeserializeFromFile(filePath, _serializerContext.VirtualAmiiboFile);
|
virtualAmiiboFile = JsonHelper.DeserializeFromFile(filePath, _serializerContext.VirtualAmiiboFile);
|
||||||
|
|
||||||
|
if (virtualAmiiboFile.Nickname == null)
|
||||||
|
{
|
||||||
|
virtualAmiiboFile.Nickname = "Ryujinx";
|
||||||
|
|
||||||
|
SaveAmiiboFile(virtualAmiiboFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
virtualAmiiboFile = new VirtualAmiiboFile()
|
virtualAmiiboFile = new VirtualAmiiboFile()
|
||||||
{
|
{
|
||||||
FileVersion = 0,
|
FileVersion = 0,
|
||||||
|
Nickname = "Ryujinx",
|
||||||
TagUuid = Array.Empty<byte>(),
|
TagUuid = Array.Empty<byte>(),
|
||||||
AmiiboId = amiiboId,
|
AmiiboId = amiiboId,
|
||||||
FirstWriteDate = DateTime.Now,
|
FirstWriteDate = DateTime.Now,
|
||||||
|
@ -194,6 +214,28 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
||||||
return virtualAmiiboFile;
|
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)
|
private static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile)
|
||||||
{
|
{
|
||||||
string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json");
|
string filePath = Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json");
|
||||||
|
|
Loading…
Reference in a new issue