First attempt
Currently DateTime is incorrect, Still trying to figure out application data.
This commit is contained in:
parent
08b7257be5
commit
6b155f5fbe
@ -16,6 +16,7 @@ using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemA
|
|||||||
using Ryujinx.HLE.HOS.Services.Apm;
|
using Ryujinx.HLE.HOS.Services.Apm;
|
||||||
using Ryujinx.HLE.HOS.Services.Caps;
|
using Ryujinx.HLE.HOS.Services.Caps;
|
||||||
using Ryujinx.HLE.HOS.Services.Mii;
|
using Ryujinx.HLE.HOS.Services.Mii;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Nfc.Bin;
|
||||||
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
||||||
using Ryujinx.HLE.HOS.Services.Nv;
|
using Ryujinx.HLE.HOS.Services.Nv;
|
||||||
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
|
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
|
||||||
@ -344,6 +345,17 @@ namespace Ryujinx.HLE.HOS
|
|||||||
NfpDevices[nfpDeviceId].UseRandomUuid = useRandomUuid;
|
NfpDevices[nfpDeviceId].UseRandomUuid = useRandomUuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public void ScanAmiiboFromBin(string path)
|
||||||
|
{
|
||||||
|
byte[] encryptedData = File.ReadAllBytes(path);
|
||||||
|
VirtualAmiiboFile newFile = AmiiboBinReader.ReadBinFile(encryptedData);
|
||||||
|
if (SearchingForAmiibo(out int nfpDeviceId))
|
||||||
|
{
|
||||||
|
NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
|
||||||
|
NfpDevices[nfpDeviceId].AmiiboId = newFile.AmiiboId;
|
||||||
|
NfpDevices[nfpDeviceId].UseRandomUuid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool SearchingForAmiibo(out int nfpDeviceId)
|
public bool SearchingForAmiibo(out int nfpDeviceId)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,233 @@
|
|||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
||||||
|
using Ryujinx.HLE.HOS.Tamper;
|
||||||
|
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.Bin
|
||||||
|
{
|
||||||
|
public class AmiiboBinReader
|
||||||
|
{
|
||||||
|
// Method to calculate BCC (XOR checksum) from UID bytes
|
||||||
|
private static byte CalculateBCC(byte[] uid, int startIdx)
|
||||||
|
{
|
||||||
|
return (byte)(uid[startIdx] ^ uid[startIdx + 1] ^ uid[startIdx + 2] ^ 0x88);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to read and process a .bin file
|
||||||
|
public static VirtualAmiiboFile ReadBinFile(byte[] fileBytes)
|
||||||
|
{
|
||||||
|
string keyRetailBinPath = GetKeyRetailBinPath();
|
||||||
|
if (string.IsNullOrEmpty(keyRetailBinPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Key retail bin path not found.");
|
||||||
|
return new VirtualAmiiboFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] initialCounter = new byte[16];
|
||||||
|
|
||||||
|
// Ensure the file is long enough
|
||||||
|
if (fileBytes.Length < 128 * 4) // Each page is 4 bytes, total 512 bytes
|
||||||
|
{
|
||||||
|
Console.WriteLine("File is too short to process.");
|
||||||
|
return new VirtualAmiiboFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the Amiibo data
|
||||||
|
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
|
||||||
|
byte[] decryptedFileBytes = amiiboDecryptor.DecryptAmiiboData(fileBytes, initialCounter);
|
||||||
|
|
||||||
|
// Assuming the UID is stored in the first 7 bytes (NTAG215 UID length)
|
||||||
|
byte[] uid = new byte[7];
|
||||||
|
Array.Copy(fileBytes, 0, uid, 0, 7);
|
||||||
|
|
||||||
|
// Calculate BCC values
|
||||||
|
byte bcc0 = CalculateBCC(uid, 0); // BCC0 = UID0 ^ UID1 ^ UID2 ^ 0x88
|
||||||
|
byte bcc1 = CalculateBCC(uid, 3); // BCC1 = UID3 ^ UID4 ^ UID5 ^ 0x00
|
||||||
|
|
||||||
|
Console.WriteLine($"UID: {BitConverter.ToString(uid)}");
|
||||||
|
Console.WriteLine($"BCC0: 0x{bcc0:X2}, BCC1: 0x{bcc1:X2}");
|
||||||
|
|
||||||
|
// Initialize byte arrays for data extraction
|
||||||
|
byte[] nickNameBytes = new byte[20]; // Amiibo nickname is 20 bytes
|
||||||
|
byte[] titleId = new byte[8];
|
||||||
|
byte[] usedCharacter = new byte[2];
|
||||||
|
byte[] variation = new byte[2];
|
||||||
|
byte[] amiiboID = new byte[2];
|
||||||
|
byte[] setID = new byte[1];
|
||||||
|
byte[] initDate = new byte[2];
|
||||||
|
byte[] writeDate = new byte[2];
|
||||||
|
byte[] writeCounter = new byte[2];
|
||||||
|
byte formData = 0;
|
||||||
|
byte[] applicationAreas = new byte[212];
|
||||||
|
|
||||||
|
// Reading specific pages and parsing bytes
|
||||||
|
for (int page = 0; page < 128; page++) // NTAG215 has 128 pages
|
||||||
|
{
|
||||||
|
int pageStartIdx = page * 4; // Each page is 4 bytes
|
||||||
|
byte[] pageData = new byte[4];
|
||||||
|
bool isEncrypted = IsPageEncrypted(page);
|
||||||
|
byte[] sourceBytes = isEncrypted ? decryptedFileBytes : fileBytes;
|
||||||
|
Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4);
|
||||||
|
|
||||||
|
// Special handling for specific pages
|
||||||
|
switch (page)
|
||||||
|
{
|
||||||
|
case 0: // Page 0 (UID + BCC0)
|
||||||
|
Console.WriteLine("Page 0: UID and BCC0.");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2: // Page 2 (BCC1 + Internal Value)
|
||||||
|
byte internalValue = pageData[1];
|
||||||
|
Console.WriteLine($"Page 2: BCC1 + Internal Value 0x{internalValue:X2} (Expected 0x48).");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
// Bytes 0 and 1 are init date, bytes 2 and 3 are write date
|
||||||
|
Array.Copy(pageData, 0, initDate, 0, 2);
|
||||||
|
Array.Copy(pageData, 2, writeDate, 0, 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 8:
|
||||||
|
case 9:
|
||||||
|
case 10:
|
||||||
|
case 11:
|
||||||
|
case 12:
|
||||||
|
// Extract nickname bytes
|
||||||
|
int nickNameOffset = (page - 8) * 4;
|
||||||
|
Array.Copy(pageData, 0, nickNameBytes, nickNameOffset, 4);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 21:
|
||||||
|
// Bytes 0 and 1 are used character, bytes 2 and 3 are variation
|
||||||
|
Array.Copy(pageData, 0, usedCharacter, 0, 2);
|
||||||
|
Array.Copy(pageData, 2, variation, 0, 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 22:
|
||||||
|
// Bytes 0 and 1 are amiibo ID, byte 2 is set ID, byte 3 is form data
|
||||||
|
Array.Copy(pageData, 0, amiiboID, 0, 2);
|
||||||
|
setID[0] = pageData[2];
|
||||||
|
formData = pageData[3];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 64:
|
||||||
|
case 65:
|
||||||
|
// Extract title ID
|
||||||
|
int titleIdOffset = (page - 64) * 4;
|
||||||
|
Array.Copy(pageData, 0, titleId, titleIdOffset, 4);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 66:
|
||||||
|
// Bytes 0 and 1 are write counter
|
||||||
|
Array.Copy(pageData, 0, writeCounter, 0, 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Pages 76 to 127 are application areas
|
||||||
|
case >= 76 and <= 127:
|
||||||
|
int appAreaOffset = (page - 76) * 4;
|
||||||
|
Array.Copy(pageData, 0, applicationAreas, appAreaOffset, 4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Debugging
|
||||||
|
string titleIdStr = BitConverter.ToString(titleId).Replace("-", "");
|
||||||
|
string usedCharacterStr = BitConverter.ToString(usedCharacter).Replace("-", "");
|
||||||
|
string variationStr = BitConverter.ToString(variation).Replace("-", "");
|
||||||
|
string amiiboIDStr = BitConverter.ToString(amiiboID).Replace("-", "");
|
||||||
|
string formDataStr = formData.ToString("X2");
|
||||||
|
string setIDStr = BitConverter.ToString(setID).Replace("-", "");
|
||||||
|
string nickName = Encoding.BigEndianUnicode.GetString(nickNameBytes).TrimEnd('\0');
|
||||||
|
string head = usedCharacterStr + variationStr;
|
||||||
|
string tail = amiiboIDStr + setIDStr + "02";
|
||||||
|
string finalID = head + tail;
|
||||||
|
string initDateStr = BitConverter.ToString(initDate).Replace("-", "");
|
||||||
|
string writeDateStr = BitConverter.ToString(writeDate).Replace("-", "");
|
||||||
|
|
||||||
|
Console.WriteLine($"Title ID: {titleIdStr}");
|
||||||
|
Console.WriteLine($"Head: {head}");
|
||||||
|
Console.WriteLine($"Tail: {tail}");
|
||||||
|
Console.WriteLine($"Used Character: {usedCharacterStr}");
|
||||||
|
Console.WriteLine($"Form Data: {formDataStr}");
|
||||||
|
Console.WriteLine($"Variation: {variationStr}");
|
||||||
|
Console.WriteLine($"Amiibo ID: {amiiboIDStr}");
|
||||||
|
Console.WriteLine($"Set ID: {setIDStr}");
|
||||||
|
Console.WriteLine($"Final ID: {finalID}");
|
||||||
|
Console.WriteLine($"Nickname: {nickName}");
|
||||||
|
Console.WriteLine($"Init Date: {initDateStr}");
|
||||||
|
Console.WriteLine($"Write Date: {writeDateStr}");
|
||||||
|
|
||||||
|
VirtualAmiiboFile virtualAmiiboFile = new VirtualAmiiboFile
|
||||||
|
{
|
||||||
|
FileVersion = 1,
|
||||||
|
TagUuid = uid,
|
||||||
|
AmiiboId = finalID
|
||||||
|
};
|
||||||
|
|
||||||
|
DateTime initDateTime = DateTimeFromBytes(initDate);
|
||||||
|
DateTime writeDateTime = DateTimeFromBytes(writeDate);
|
||||||
|
|
||||||
|
Console.WriteLine($"Parsed Init Date: {initDateTime}");
|
||||||
|
Console.WriteLine($"Parsed Write Date: {writeDateTime}");
|
||||||
|
|
||||||
|
virtualAmiiboFile.FirstWriteDate = initDateTime;
|
||||||
|
virtualAmiiboFile.LastWriteDate = writeDateTime;
|
||||||
|
virtualAmiiboFile.WriteCounter = BitConverter.ToUInt16(writeCounter, 0);
|
||||||
|
|
||||||
|
// Parse application areas
|
||||||
|
//List<VirtualAmiiboApplicationArea> applicationAreasList = ParseAmiiboData(applicationAreas);
|
||||||
|
List<VirtualAmiiboApplicationArea> applicationAreasList = new List<VirtualAmiiboApplicationArea>();
|
||||||
|
virtualAmiiboFile.ApplicationAreas = applicationAreasList;
|
||||||
|
|
||||||
|
// Save the virtual Amiibo file
|
||||||
|
VirtualAmiibo.SaveAmiiboFile(virtualAmiiboFile);
|
||||||
|
|
||||||
|
return virtualAmiiboFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetKeyRetailBinPath()
|
||||||
|
{
|
||||||
|
return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsPageEncrypted(int page)
|
||||||
|
{
|
||||||
|
return (page >= 6 && page <= 9) || (page >= 43 && page <= 84);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime DateTimeFromBytes(byte[] date)
|
||||||
|
{
|
||||||
|
if (date == null || date.Length != 2)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Invalid date bytes.");
|
||||||
|
return DateTime.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ushort value = BitConverter.ToUInt16(date, 0);
|
||||||
|
|
||||||
|
int day = value & 0x1F;
|
||||||
|
int month = (value >> 5) & 0x0F;
|
||||||
|
int year = (value >> 9) & 0x7F;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new DateTime(2000 + year, month, day);
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Invalid date values extracted.");
|
||||||
|
return DateTime.MinValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<VirtualAmiiboApplicationArea> ParseAmiiboData(byte[] decryptedData)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<List<VirtualAmiiboApplicationArea>>(decryptedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Mii.Types;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
||||||
|
{
|
||||||
|
public class AmiiboDecrypter
|
||||||
|
{
|
||||||
|
public readonly byte[] _hmacKey; // HMAC key
|
||||||
|
public readonly byte[] _aesKey; // AES key
|
||||||
|
|
||||||
|
public AmiiboDecrypter(string keyRetailBinPath)
|
||||||
|
{
|
||||||
|
var keys = AmiiboMasterKey.FromCombinedBin(File.ReadAllBytes(keyRetailBinPath));
|
||||||
|
_hmacKey = keys.DataKey.HmacKey;
|
||||||
|
_aesKey = keys.DataKey.XorPad;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] DecryptAmiiboData(byte[] encryptedData, byte[] counter)
|
||||||
|
{
|
||||||
|
// Ensure the counter length matches the block size
|
||||||
|
if (counter.Length != 16)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Counter must be 16 bytes long for AES block size.");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] decryptedData = new byte[encryptedData.Length];
|
||||||
|
|
||||||
|
using (Aes aesAlg = Aes.Create())
|
||||||
|
{
|
||||||
|
aesAlg.Key = _aesKey;
|
||||||
|
aesAlg.Mode = CipherMode.ECB; // Use ECB mode to handle the counter encryption
|
||||||
|
aesAlg.Padding = PaddingMode.None;
|
||||||
|
|
||||||
|
using (var encryptor = aesAlg.CreateEncryptor())
|
||||||
|
{
|
||||||
|
int blockSize = 16;
|
||||||
|
byte[] encryptedCounter = new byte[blockSize];
|
||||||
|
byte[] currentCounter = (byte[])counter.Clone();
|
||||||
|
|
||||||
|
for (int i = 0; i < encryptedData.Length; i += blockSize)
|
||||||
|
{
|
||||||
|
// Encrypt the current counter block
|
||||||
|
encryptor.TransformBlock(currentCounter, 0, blockSize, encryptedCounter, 0);
|
||||||
|
|
||||||
|
// XOR the encrypted counter with the ciphertext to get the decrypted data
|
||||||
|
for (int j = 0; j < blockSize && i + j < encryptedData.Length; j++)
|
||||||
|
{
|
||||||
|
decryptedData[i + j] = (byte)(encryptedData[i + j] ^ encryptedCounter[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the counter for the next block
|
||||||
|
IncrementCounter(currentCounter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] CalculateHMAC(byte[] data)
|
||||||
|
{
|
||||||
|
using (var hmac = new HMACSHA256(_hmacKey))
|
||||||
|
{
|
||||||
|
return hmac.ComputeHash(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void IncrementCounter(byte[] counter)
|
||||||
|
{
|
||||||
|
for (int i = counter.Length - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (++counter[i] != 0)
|
||||||
|
break; // Stop if no overflow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime ParseDate(byte[] data, int offset)
|
||||||
|
{
|
||||||
|
ushort year = BitConverter.ToUInt16(data, offset);
|
||||||
|
byte month = data[offset + 2];
|
||||||
|
byte day = data[offset + 3];
|
||||||
|
byte hour = data[offset + 4];
|
||||||
|
byte minute = data[offset + 5];
|
||||||
|
byte second = data[offset + 6];
|
||||||
|
|
||||||
|
return new DateTime(year, month, day, hour, minute, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<object> ParseApplicationAreas(byte[] data, int startOffset, int areaSize)
|
||||||
|
{
|
||||||
|
var areas = new List<object>();
|
||||||
|
for (int i = 0; i < 8; i++) // Assuming 8 areas
|
||||||
|
{
|
||||||
|
int offset = startOffset + (i * areaSize);
|
||||||
|
string applicationId = BitConverter.ToString(data[offset..(offset + 4)]).Replace("-", "");
|
||||||
|
byte[] areaData = data[(offset + 4)..(offset + areaSize)];
|
||||||
|
areas.Add(new VirtualAmiiboApplicationArea
|
||||||
|
{
|
||||||
|
ApplicationAreaId = uint.Parse(applicationId),
|
||||||
|
ApplicationArea = areaData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return areas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Ryujinx.HLE.HOS.Services.Nfc.Bin
|
||||||
|
{
|
||||||
|
public class AmiiboMasterKey
|
||||||
|
{
|
||||||
|
private const int DataLength = 80;
|
||||||
|
private const int CombinedLength = 160;
|
||||||
|
public byte[] HmacKey { get; private set; } // 16 bytes
|
||||||
|
public byte[] TypeString { get; private set; } // 14 bytes
|
||||||
|
public byte Rfu { get; private set; } // 1 byte reserved
|
||||||
|
public byte MagicSize { get; private set; } // 1 byte
|
||||||
|
public byte[] MagicBytes { get; private set; } // 16 bytes
|
||||||
|
public byte[] XorPad { get; private set; } // 32 bytes
|
||||||
|
|
||||||
|
private AmiiboMasterKey(byte[] data)
|
||||||
|
{
|
||||||
|
if (data.Length != DataLength)
|
||||||
|
throw new ArgumentException($"Data is {data.Length} bytes (should be {DataLength}).");
|
||||||
|
|
||||||
|
|
||||||
|
// Unpack the data
|
||||||
|
HmacKey = data[..16];
|
||||||
|
TypeString = data[16..30];
|
||||||
|
Rfu = data[30];
|
||||||
|
MagicSize = data[31];
|
||||||
|
MagicBytes = data[32..48];
|
||||||
|
XorPad = data[48..];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromSeparateBin(byte[] dataBin, byte[] tagBin)
|
||||||
|
{
|
||||||
|
var dataKey = new AmiiboMasterKey(dataBin);
|
||||||
|
var tagKey = new AmiiboMasterKey(tagBin);
|
||||||
|
return (dataKey, tagKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromSeparateHex(string dataHex, string tagHex)
|
||||||
|
{
|
||||||
|
return FromSeparateBin(HexToBytes(dataHex), HexToBytes(tagHex));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromCombinedBin(byte[] combinedBin)
|
||||||
|
{
|
||||||
|
if (combinedBin.Length != CombinedLength)
|
||||||
|
throw new ArgumentException($"Data is {combinedBin.Length} bytes (should be {CombinedLength}).");
|
||||||
|
|
||||||
|
byte[] dataBin = combinedBin[..DataLength];
|
||||||
|
byte[] tagBin = combinedBin[DataLength..];
|
||||||
|
return FromSeparateBin(dataBin, tagBin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] HexToBytes(string hex)
|
||||||
|
{
|
||||||
|
int length = hex.Length / 2;
|
||||||
|
byte[] bytes = new byte[length];
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager
|
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager
|
||||||
{
|
{
|
||||||
struct VirtualAmiiboFile
|
public struct VirtualAmiiboFile
|
||||||
{
|
{
|
||||||
public uint FileVersion { get; set; }
|
public uint FileVersion { get; set; }
|
||||||
public byte[] TagUuid { get; set; }
|
public byte[] TagUuid { get; set; }
|
||||||
@ -15,7 +15,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager
|
|||||||
public List<VirtualAmiiboApplicationArea> ApplicationAreas { get; set; }
|
public List<VirtualAmiiboApplicationArea> ApplicationAreas { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VirtualAmiiboApplicationArea
|
public struct VirtualAmiiboApplicationArea
|
||||||
{
|
{
|
||||||
public uint ApplicationAreaId { get; set; }
|
public uint ApplicationAreaId { get; set; }
|
||||||
public byte[] ApplicationArea { get; set; }
|
public byte[] ApplicationArea { get; set; }
|
||||||
|
@ -204,7 +204,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
|
|||||||
return virtualAmiiboFile;
|
return virtualAmiiboFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SaveAmiiboFile(VirtualAmiiboFile virtualAmiiboFile)
|
public 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");
|
||||||
JsonHelper.SerializeToFile(filePath, virtualAmiiboFile, _serializerContext.VirtualAmiiboFile);
|
JsonHelper.SerializeToFile(filePath, virtualAmiiboFile, _serializerContext.VirtualAmiiboFile);
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
"MenuBarActions": "_Actions",
|
"MenuBarActions": "_Actions",
|
||||||
"MenuBarOptionsSimulateWakeUpMessage": "Simulate Wake-up message",
|
"MenuBarOptionsSimulateWakeUpMessage": "Simulate Wake-up message",
|
||||||
"MenuBarActionsScanAmiibo": "Scan An Amiibo",
|
"MenuBarActionsScanAmiibo": "Scan An Amiibo",
|
||||||
|
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
|
||||||
"MenuBarTools": "_Tools",
|
"MenuBarTools": "_Tools",
|
||||||
"MenuBarToolsInstallFirmware": "Install Firmware",
|
"MenuBarToolsInstallFirmware": "Install Firmware",
|
||||||
"MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP",
|
"MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP",
|
||||||
|
@ -28,12 +28,14 @@ using Ryujinx.HLE;
|
|||||||
using Ryujinx.HLE.FileSystem;
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS;
|
using Ryujinx.HLE.HOS;
|
||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
|
using Ryujinx.HLE.HOS.Services.Nfc.Bin;
|
||||||
using Ryujinx.HLE.UI;
|
using Ryujinx.HLE.UI;
|
||||||
using Ryujinx.Input.HLE;
|
using Ryujinx.Input.HLE;
|
||||||
using Ryujinx.UI.App.Common;
|
using Ryujinx.UI.App.Common;
|
||||||
using Ryujinx.UI.Common;
|
using Ryujinx.UI.Common;
|
||||||
using Ryujinx.UI.Common.Configuration;
|
using Ryujinx.UI.Common.Configuration;
|
||||||
using Ryujinx.UI.Common.Helper;
|
using Ryujinx.UI.Common.Helper;
|
||||||
|
using Silk.NET.Vulkan;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -2059,6 +2061,32 @@ namespace Ryujinx.Ava.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public async Task OpenBinFile()
|
||||||
|
{
|
||||||
|
if (!IsAmiiboRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (AppHost.Device.System.SearchingForAmiibo(out int deviceId))
|
||||||
|
{
|
||||||
|
var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
|
||||||
|
AllowMultiple = false,
|
||||||
|
FileTypeFilter = new List<FilePickerFileType>
|
||||||
|
{
|
||||||
|
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||||
|
{
|
||||||
|
Patterns = new[] { "*.bin" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (result.Count > 0)
|
||||||
|
{
|
||||||
|
AppHost.Device.System.ScanAmiiboFromBin(result[0].Path.LocalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void ToggleFullscreen()
|
public void ToggleFullscreen()
|
||||||
{
|
{
|
||||||
|
@ -241,6 +241,13 @@
|
|||||||
Icon="{ext:Icon mdi-cube-scan}"
|
Icon="{ext:Icon mdi-cube-scan}"
|
||||||
InputGesture="Ctrl + A"
|
InputGesture="Ctrl + A"
|
||||||
IsEnabled="{Binding IsAmiiboRequested}" />
|
IsEnabled="{Binding IsAmiiboRequested}" />
|
||||||
|
<MenuItem
|
||||||
|
Name="ScanAmiiboMenuItemFromBin"
|
||||||
|
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
|
||||||
|
Click="OpenBinFile"
|
||||||
|
Header="{ext:Locale MenuBarActionsScanAmiiboBin}"
|
||||||
|
Icon="{ext:Icon mdi-cube-scan}"
|
||||||
|
IsEnabled="{Binding IsAmiiboRequested}" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
Command="{Binding TakeScreenshot}"
|
Command="{Binding TakeScreenshot}"
|
||||||
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"
|
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"
|
||||||
|
@ -141,6 +141,9 @@ namespace Ryujinx.Ava.UI.Views.Main
|
|||||||
public async void OpenAmiiboWindow(object sender, RoutedEventArgs e)
|
public async void OpenAmiiboWindow(object sender, RoutedEventArgs e)
|
||||||
=> await ViewModel.OpenAmiiboWindow();
|
=> await ViewModel.OpenAmiiboWindow();
|
||||||
|
|
||||||
|
public async void OpenBinFile(object sender, RoutedEventArgs e)
|
||||||
|
=> await ViewModel.OpenBinFile();
|
||||||
|
|
||||||
public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e)
|
public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!ViewModel.IsGameRunning)
|
if (!ViewModel.IsGameRunning)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user