Compare commits

...

13 commits

Author SHA1 Message Date
Jacobwasbeast
e5feb680af
Merge f5b24796cf into b3944a18b7 2024-12-12 13:47:29 -06:00
Evan Husted
f5b24796cf
Merge branch 'master' into feature/amiibo-bin-reading 2024-12-12 13:47:26 -06:00
Jacobwasbeast
46e5f566b9 Fix casing inconsistencies and remove unused showRightmostSeparator property 2024-12-12 13:33:11 -06:00
rrondo
b3944a18b7
Some changes to Ukrainian localization (uk_UA.json) (#356)
Some checks failed
Canary release job / Create tag (push) Has been cancelled
Canary release job / Release for linux-arm64 (push) Has been cancelled
Canary release job / Release for linux-x64 (push) Has been cancelled
Canary release job / Release for win-x64 (push) Has been cancelled
Canary release job / Release MacOS universal (push) Has been cancelled
I made some small corrections and translated the lines that didn’t have
translations.
2024-12-10 11:58:53 -06:00
Jacobwasbeast
35a62be2b1
Merge branch 'master' into feature/amiibo-bin-reading 2024-12-10 10:32:16 -06:00
Jacobwasbeast
a93b42d218 Fix Logical Inconsistencies 2024-12-10 10:29:44 -06:00
Jacobwasbeast
bc79830b3b Finishing touches 2024-12-05 03:00:10 -06:00
Jacobwasbeast
6051c35553 Added nickname support 2024-12-05 02:38:30 -06:00
Jacobwasbeast
e131901de3 Added Saving 2024-12-05 02:26:11 -06:00
Jacobwasbeast
e6b11ad76d Actually got it working
Nickname changing and saving does not work, if you try to save with a bin amiibo it will just save over your previous virtual amiibo.
2024-12-05 01:33:51 -06:00
Jacobwasbeast
334e71528e Simplify The Logic 2024-12-04 20:47:04 -06:00
Jacobwasbeast
46f8b8fa85 Some small fixes
The data now loads but SMBU says it can't be loaded
The date time is for some reason in the future idk why still working on it
2024-12-04 08:34:58 -06:00
Jacobwasbeast
6b155f5fbe First attempt
Currently DateTime is incorrect, Still trying to figure out application data.
2024-12-03 21:50:33 -06:00
29 changed files with 1113 additions and 162 deletions

View file

@ -16,6 +16,8 @@ using Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemA
using Ryujinx.HLE.HOS.Services.Apm;
using Ryujinx.HLE.HOS.Services.Caps;
using Ryujinx.HLE.HOS.Services.Mii;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
using Ryujinx.HLE.HOS.Services.Nv;
using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl;
@ -337,6 +339,11 @@ namespace Ryujinx.HLE.HOS
public void ScanAmiibo(int nfpDeviceId, string amiiboId, bool useRandomUuid)
{
if (VirtualAmiibo.ApplicationBytes.Length > 0)
{
VirtualAmiibo.ApplicationBytes = new byte[0];
VirtualAmiibo.InputBin = string.Empty;
}
if (NfpDevices[nfpDeviceId].State == NfpDeviceState.SearchingForTag)
{
NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound;
@ -344,6 +351,22 @@ namespace Ryujinx.HLE.HOS
NfpDevices[nfpDeviceId].UseRandomUuid = useRandomUuid;
}
}
public void ScanAmiiboFromBin(string path)
{
VirtualAmiibo.InputBin = path;
if (VirtualAmiibo.ApplicationBytes.Length > 0)
{
VirtualAmiibo.ApplicationBytes = new byte[0];
}
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)
{

View file

@ -0,0 +1,340 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
using System;
using System.IO;
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
{
public class AmiiboBinReader
{
private static byte CalculateBCC0(byte[] uid)
{
return (byte)(uid[0] ^ uid[1] ^ uid[2] ^ 0x88);
}
private static byte CalculateBCC1(byte[] uid)
{
return (byte)(uid[3] ^ uid[4] ^ uid[5] ^ uid[6]);
}
public static VirtualAmiiboFile ReadBinFile(byte[] fileBytes)
{
string keyRetailBinPath = GetKeyRetailBinPath();
if (string.IsNullOrEmpty(keyRetailBinPath))
{
return new VirtualAmiiboFile();
}
byte[] initialCounter = new byte[16];
const int totalPages = 135;
const int pageSize = 4;
const int totalBytes = totalPages * pageSize;
if (fileBytes.Length < totalBytes)
{
return new VirtualAmiiboFile();
}
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(fileBytes);
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[] appId = new byte[8];
byte[] settingsBytes = new byte[2];
byte formData = 0;
byte[] applicationAreas = new byte[216];
byte[] dataFull = amiiboDump.GetData();
Logger.Debug?.Print(LogClass.ServiceNfp, $"Data Full Length: {dataFull.Length}");
byte[] uid = new byte[7];
Array.Copy(dataFull, 0, uid, 0, 7);
byte bcc0 = CalculateBCC0(uid);
byte bcc1 = CalculateBCC1(uid);
LogDebugData(uid, bcc0, bcc1);
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];
byte[] sourceBytes = dataFull;
Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4);
// Special handling for specific pages
switch (page)
{
case 0: // Page 0 (UID + BCC0)
Logger.Debug?.Print(LogClass.ServiceNfp, "Page 0: UID and BCC0.");
break;
case 2: // Page 2 (BCC1 + Internal Value)
byte internalValue = pageData[1];
Logger.Debug?.Print(LogClass.ServiceNfp, $"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 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;
}
}
string usedCharacterStr = BitConverter.ToString(usedCharacter).Replace("-", "");
string variationStr = BitConverter.ToString(variation).Replace("-", "");
string amiiboIDStr = BitConverter.ToString(amiiboID).Replace("-", "");
string setIDStr = BitConverter.ToString(setID).Replace("-", "");
string head = usedCharacterStr + variationStr;
string tail = amiiboIDStr + setIDStr + "02";
string finalID = head + tail;
ushort settingsValue = BitConverter.ToUInt16(settingsBytes, 0);
ushort initDateValue = BitConverter.ToUInt16(initDate, 0);
ushort writeDateValue = BitConverter.ToUInt16(writeDate, 0);
DateTime initDateTime = DateTimeFromTag(initDateValue);
DateTime writeDateTime = DateTimeFromTag(writeDateValue);
ushort writeCounterValue = BitConverter.ToUInt16(writeCounter, 0);
string nickName = amiiboDump.AmiiboNickname;
LogFinalData(titleId, appId, head, tail, finalID, nickName, initDateTime, writeDateTime, settingsValue, writeCounterValue, applicationAreas);
VirtualAmiiboFile virtualAmiiboFile = new VirtualAmiiboFile
{
FileVersion = 1,
TagUuid = uid,
AmiiboId = finalID,
NickName = nickName,
FirstWriteDate = initDateTime,
LastWriteDate = writeDateTime,
WriteCounter = writeCounterValue,
};
if (writeCounterValue > 0)
{
VirtualAmiibo.ApplicationBytes = applicationAreas;
}
VirtualAmiibo.NickName = nickName;
return virtualAmiiboFile;
}
public static bool SaveBinFile(string inputFile, byte[] appData)
{
Logger.Info?.Print(LogClass.ServiceNfp, "Saving bin file.");
byte[] readBytes;
try
{
readBytes = File.ReadAllBytes(inputFile);
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.ServiceNfp, $"Error reading file: {ex.Message}");
return false;
}
string keyRetailBinPath = GetKeyRetailBinPath();
if (string.IsNullOrEmpty(keyRetailBinPath))
{
Logger.Error?.Print(LogClass.ServiceNfp, "Key retail path is empty.");
return false;
}
if (appData.Length != 216) // Ensure application area size is valid
{
Logger.Error?.Print(LogClass.ServiceNfp, "Invalid application data length. Expected 216 bytes.");
return false;
}
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(readBytes);
byte[] oldData = amiiboDump.GetData();
if (oldData.Length != 540) // Verify the expected length for NTAG215 tags
{
Logger.Error?.Print(LogClass.ServiceNfp, "Invalid tag data length. Expected 540 bytes.");
return false;
}
byte[] newData = new byte[oldData.Length];
Array.Copy(oldData, newData, oldData.Length);
// Replace application area with appData
int appAreaOffset = 76 * 4; // Starting page (76) times 4 bytes per page
Array.Copy(appData, 0, newData, appAreaOffset, appData.Length);
AmiiboDump encryptedDump = amiiboDecryptor.EncryptAmiiboDump(newData);
byte[] encryptedData = encryptedDump.GetData();
if (encryptedData == null || encryptedData.Length != readBytes.Length)
{
Logger.Error?.Print(LogClass.ServiceNfp, "Failed to encrypt data correctly.");
return false;
}
inputFile = inputFile.Replace("_modified", string.Empty);
// Save the encrypted data to file or return it for saving externally
string outputFilePath = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_modified.bin");
try
{
File.WriteAllBytes(outputFilePath, encryptedData);
Logger.Info?.Print(LogClass.ServiceNfp, $"Modified Amiibo data saved to {outputFilePath}.");
return true;
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.ServiceNfp, $"Error saving file: {ex.Message}");
return false;
}
}
public static bool SaveBinFile(string inputFile, string newNickName)
{
Logger.Info?.Print(LogClass.ServiceNfp, "Saving bin file.");
byte[] readBytes;
try
{
readBytes = File.ReadAllBytes(inputFile);
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.ServiceNfp, $"Error reading file: {ex.Message}");
return false;
}
string keyRetailBinPath = GetKeyRetailBinPath();
if (string.IsNullOrEmpty(keyRetailBinPath))
{
Logger.Error?.Print(LogClass.ServiceNfp, "Key retail path is empty.");
return false;
}
AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath);
AmiiboDump amiiboDump = amiiboDecryptor.DecryptAmiiboDump(readBytes);
amiiboDump.AmiiboNickname = newNickName;
byte[] oldData = amiiboDump.GetData();
if (oldData.Length != 540) // Verify the expected length for NTAG215 tags
{
Logger.Error?.Print(LogClass.ServiceNfp, "Invalid tag data length. Expected 540 bytes.");
return false;
}
byte[] encryptedData = amiiboDecryptor.EncryptAmiiboDump(oldData).GetData();
if (encryptedData == null || encryptedData.Length != readBytes.Length)
{
Logger.Error?.Print(LogClass.ServiceNfp, "Failed to encrypt data correctly.");
return false;
}
inputFile = inputFile.Replace("_modified", string.Empty);
// Save the encrypted data to file or return it for saving externally
string outputFilePath = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile) + "_modified.bin");
try
{
File.WriteAllBytes(outputFilePath, encryptedData);
Logger.Info?.Print(LogClass.ServiceNfp, $"Modified Amiibo data saved to {outputFilePath}.");
return true;
}
catch (Exception ex)
{
Logger.Error?.Print(LogClass.ServiceNfp, $"Error saving file: {ex.Message}");
return false;
}
}
private static void LogDebugData(byte[] uid, byte bcc0, byte bcc1)
{
Logger.Debug?.Print(LogClass.ServiceNfp, $"UID: {BitConverter.ToString(uid)}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"BCC0: 0x{bcc0:X2}, BCC1: 0x{bcc1:X2}");
}
private static void LogFinalData(byte[] titleId, byte[] appId, string head, string tail, string finalID, string nickName, DateTime initDateTime, DateTime writeDateTime, ushort settingsValue, ushort writeCounterValue, byte[] applicationAreas)
{
Logger.Debug?.Print(LogClass.ServiceNfp, $"Title ID: 0x{BitConverter.ToString(titleId).Replace("-", "")}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"Application Program ID: 0x{BitConverter.ToString(appId).Replace("-", "")}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"Head: {head}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"Tail: {tail}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"Final ID: {finalID}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"Nickname: {nickName}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"Init Date: {initDateTime}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"Write Date: {writeDateTime}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"Settings: 0x{settingsValue:X4}");
Logger.Debug?.Print(LogClass.ServiceNfp, $"Write Counter: {writeCounterValue}");
Logger.Debug?.Print(LogClass.ServiceNfp, "Length of Application Areas: " + applicationAreas.Length);
}
private static uint CalculateCRC32(byte[] input)
{
uint[] table = new uint[256];
uint polynomial = 0xEDB88320;
for (uint i = 0; i < table.Length; ++i)
{
uint crc = i;
for (int j = 0; j < 8; ++j)
{
if ((crc & 1) != 0)
crc = (crc >> 1) ^ polynomial;
else
crc >>= 1;
}
table[i] = crc;
}
uint result = 0xFFFFFFFF;
foreach (byte b in input)
{
byte index = (byte)((result & 0xFF) ^ b);
result = (result >> 8) ^ table[index];
}
return ~result;
}
private static string GetKeyRetailBinPath()
{
return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin");
}
public static bool HasKeyRetailBinPath()
{
return File.Exists(GetKeyRetailBinPath());
}
public static DateTime DateTimeFromTag(ushort value)
{
try
{
int day = value & 0x1F;
int month = (value >> 5) & 0x0F;
int year = (value >> 9) & 0x7F;
if (day == 0 || month == 0 || month > 12 || day > DateTime.DaysInMonth(2000 + year, month))
throw new ArgumentOutOfRangeException();
return new DateTime(2000 + year, month, day);
}
catch
{
return DateTime.Now;
}
}
}
}

View file

@ -0,0 +1,43 @@
using System.IO;
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
{
public class AmiiboDecrypter
{
public AmiiboMasterKey DataKey { get; private set; }
public AmiiboMasterKey TagKey { get; private set; }
public AmiiboDecrypter(string keyRetailBinPath)
{
var combinedKeys = File.ReadAllBytes(keyRetailBinPath);
var keys = AmiiboMasterKey.FromCombinedBin(combinedKeys);
DataKey = keys.DataKey;
TagKey = keys.TagKey;
}
public AmiiboDump DecryptAmiiboDump(byte[] encryptedDumpData)
{
// Initialize AmiiboDump with encrypted data
AmiiboDump amiiboDump = new AmiiboDump(encryptedDumpData, DataKey, TagKey, isLocked: true);
// Unlock (decrypt) the dump
amiiboDump.Unlock();
// Optional: Verify HMACs
amiiboDump.VerifyHMACs();
return amiiboDump;
}
public AmiiboDump EncryptAmiiboDump(byte[] decryptedDumpData)
{
// Initialize AmiiboDump with decrypted data
AmiiboDump amiiboDump = new AmiiboDump(decryptedDumpData, DataKey, TagKey, isLocked: false);
// Lock (encrypt) the dump
amiiboDump.Lock();
return amiiboDump;
}
}
}

View file

@ -0,0 +1,387 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
{
public class AmiiboDump
{
private AmiiboMasterKey dataMasterKey;
private AmiiboMasterKey tagMasterKey;
private bool isLocked;
private byte[] data;
private byte[] hmacTagKey;
private byte[] hmacDataKey;
private byte[] aesKey;
private byte[] aesIv;
public AmiiboDump(byte[] dumpData, AmiiboMasterKey dataKey, AmiiboMasterKey tagKey, bool isLocked = true)
{
if (dumpData.Length < 540)
throw new ArgumentException("Incomplete dump. Amiibo data is at least 540 bytes.");
this.data = new byte[540];
Array.Copy(dumpData, this.data, dumpData.Length);
this.dataMasterKey = dataKey;
this.tagMasterKey = tagKey;
this.isLocked = isLocked;
if (!isLocked)
{
DeriveKeysAndCipher();
}
}
private byte[] DeriveKey(AmiiboMasterKey key, bool deriveAes, out byte[] derivedAesKey, out byte[] derivedAesIv)
{
List<byte> seed = new List<byte>();
// Start with the type string (14 bytes)
seed.AddRange(key.TypeString);
// Append data based on magic size
int append = 16 - key.MagicSize;
byte[] extract = new byte[16];
Array.Copy(this.data, 0x011, extract, 0, 2); // Extract two bytes from user data section
for (int i = 2; i < 16; i++)
{
extract[i] = 0x00;
}
seed.AddRange(extract.Take(append));
// Add the magic bytes
seed.AddRange(key.MagicBytes.Take(key.MagicSize));
// Extract the UID (UID is 8 bytes)
byte[] uid = new byte[8];
Array.Copy(this.data, 0x000, uid, 0, 8);
seed.AddRange(uid);
seed.AddRange(uid);
// Extract some tag data (pages 0x20 - 0x28)
byte[] user = new byte[32];
Array.Copy(this.data, 0x060, user, 0, 32);
// XOR it with the key padding (XorPad)
byte[] paddedUser = new byte[32];
for (int i = 0; i < user.Length; i++)
{
paddedUser[i] = (byte)(user[i] ^ key.XorPad[i]);
}
seed.AddRange(paddedUser);
byte[] seedBytes = seed.ToArray();
if (seedBytes.Length != 78)
{
throw new Exception("Size check for key derived seed failed");
}
byte[] hmacKey;
derivedAesKey = null;
derivedAesIv = null;
if (deriveAes)
{
// Derive AES Key and IV
var dataForAes = new byte[2 + seedBytes.Length];
dataForAes[0] = 0x00;
dataForAes[1] = 0x00; // Counter (0)
Array.Copy(seedBytes, 0, dataForAes, 2, seedBytes.Length);
byte[] derivedBytes;
using (var hmac = new HMACSHA256(key.HmacKey))
{
derivedBytes = hmac.ComputeHash(dataForAes);
}
derivedAesKey = derivedBytes.Take(16).ToArray();
derivedAesIv = derivedBytes.Skip(16).Take(16).ToArray();
// Derive HMAC Key
var dataForHmacKey = new byte[2 + seedBytes.Length];
dataForHmacKey[0] = 0x00;
dataForHmacKey[1] = 0x01; // Counter (1)
Array.Copy(seedBytes, 0, dataForHmacKey, 2, seedBytes.Length);
using (var hmac = new HMACSHA256(key.HmacKey))
{
derivedBytes = hmac.ComputeHash(dataForHmacKey);
}
hmacKey = derivedBytes.Take(16).ToArray();
}
else
{
// Derive HMAC Key only
var dataForHmacKey = new byte[2 + seedBytes.Length];
dataForHmacKey[0] = 0x00;
dataForHmacKey[1] = 0x01; // Counter (1)
Array.Copy(seedBytes, 0, dataForHmacKey, 2, seedBytes.Length);
byte[] derivedBytes;
using (var hmac = new HMACSHA256(key.HmacKey))
{
derivedBytes = hmac.ComputeHash(dataForHmacKey);
}
hmacKey = derivedBytes.Take(16).ToArray();
}
return hmacKey;
}
private void DeriveKeysAndCipher()
{
byte[] discard;
// Derive HMAC Tag Key
this.hmacTagKey = DeriveKey(this.tagMasterKey, false, out discard, out discard);
// Derive HMAC Data Key and AES Key/IV
this.hmacDataKey = DeriveKey(this.dataMasterKey, true, out aesKey, out aesIv);
}
private void DecryptData()
{
byte[] encryptedBlock = new byte[0x020 + 0x168];
Array.Copy(data, 0x014, encryptedBlock, 0, 0x020); // data[0x014:0x034]
Array.Copy(data, 0x0A0, encryptedBlock, 0x020, 0x168); // data[0x0A0:0x208]
byte[] decryptedBlock = AES_CTR_Transform(encryptedBlock, aesKey, aesIv);
// Copy decrypted data back
Array.Copy(decryptedBlock, 0, data, 0x014, 0x020);
Array.Copy(decryptedBlock, 0x020, data, 0x0A0, 0x168);
}
private void EncryptData()
{
byte[] plainBlock = new byte[0x020 + 0x168];
Array.Copy(data, 0x014, plainBlock, 0, 0x020); // data[0x014:0x034]
Array.Copy(data, 0x0A0, plainBlock, 0x020, 0x168); // data[0x0A0:0x208]
byte[] encryptedBlock = AES_CTR_Transform(plainBlock, aesKey, aesIv);
// Copy encrypted data back
Array.Copy(encryptedBlock, 0, data, 0x014, 0x020);
Array.Copy(encryptedBlock, 0x020, data, 0x0A0, 0x168);
}
private byte[] AES_CTR_Transform(byte[] data, byte[] key, byte[] iv)
{
byte[] output = new byte[data.Length];
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
int blockSize = aes.BlockSize / 8; // in bytes, should be 16
byte[] counter = new byte[blockSize];
Array.Copy(iv, counter, blockSize);
using (ICryptoTransform encryptor = aes.CreateEncryptor())
{
byte[] encryptedCounter = new byte[blockSize];
for (int i = 0; i < data.Length; i += blockSize)
{
// Encrypt the counter
encryptor.TransformBlock(counter, 0, blockSize, encryptedCounter, 0);
// Determine the number of bytes to process in this block
int blockLength = Math.Min(blockSize, data.Length - i);
// XOR the encrypted counter with the plaintext/ciphertext block
for (int j = 0; j < blockLength; j++)
{
output[i + j] = (byte)(data[i + j] ^ encryptedCounter[j]);
}
// Increment the counter
IncrementCounter(counter);
}
}
}
return output;
}
private void IncrementCounter(byte[] counter)
{
for (int i = counter.Length - 1; i >= 0; i--)
{
if (++counter[i] != 0)
break;
}
}
private void DeriveHMACs()
{
if (isLocked)
throw new InvalidOperationException("Cannot derive HMACs when data is locked.");
// Calculate tag HMAC
byte[] tagHmacData = new byte[8 + 44];
Array.Copy(data, 0x000, tagHmacData, 0, 8);
Array.Copy(data, 0x054, tagHmacData, 8, 44);
byte[] tagHmac;
using (var hmac = new HMACSHA256(hmacTagKey))
{
tagHmac = hmac.ComputeHash(tagHmacData);
}
// Overwrite the stored tag HMAC
Array.Copy(tagHmac, 0, data, 0x034, 32);
// Prepare data for data HMAC
int len1 = 0x023; // 0x011 to 0x034 (0x034 - 0x011)
int len2 = 0x168; // 0x0A0 to 0x208 (0x208 - 0x0A0)
int len3 = tagHmac.Length; // 32 bytes
int len4 = 0x008; // 0x000 to 0x008 (0x008 - 0x000)
int len5 = 0x02C; // 0x054 to 0x080 (0x080 - 0x054)
int totalLength = len1 + len2 + len3 + len4 + len5;
byte[] dataHmacData = new byte[totalLength];
int offset = 0;
Array.Copy(data, 0x011, dataHmacData, offset, len1);
offset += len1;
Array.Copy(data, 0x0A0, dataHmacData, offset, len2);
offset += len2;
Array.Copy(tagHmac, 0, dataHmacData, offset, len3);
offset += len3;
Array.Copy(data, 0x000, dataHmacData, offset, len4);
offset += len4;
Array.Copy(data, 0x054, dataHmacData, offset, len5);
byte[] dataHmac;
using (var hmac = new HMACSHA256(hmacDataKey))
{
dataHmac = hmac.ComputeHash(dataHmacData);
}
// Overwrite the stored data HMAC
Array.Copy(dataHmac, 0, data, 0x080, 32);
}
public void VerifyHMACs()
{
if (isLocked)
throw new InvalidOperationException("Cannot verify HMACs when data is locked.");
// Calculate tag HMAC
byte[] tagHmacData = new byte[8 + 44];
Array.Copy(data, 0x000, tagHmacData, 0, 8);
Array.Copy(data, 0x054, tagHmacData, 8, 44);
byte[] calculatedTagHmac;
using (var hmac = new HMACSHA256(hmacTagKey))
{
calculatedTagHmac = hmac.ComputeHash(tagHmacData);
}
byte[] storedTagHmac = new byte[32];
Array.Copy(data, 0x034, storedTagHmac, 0, 32);
if (!calculatedTagHmac.SequenceEqual(storedTagHmac))
{
throw new Exception("Tag HMAC verification failed.");
}
// Prepare data for data HMAC
int len1 = 0x023; // 0x011 to 0x034
int len2 = 0x168; // 0x0A0 to 0x208
int len3 = calculatedTagHmac.Length; // 32 bytes
int len4 = 0x008; // 0x000 to 0x008
int len5 = 0x02C; // 0x054 to 0x080
int totalLength = len1 + len2 + len3 + len4 + len5;
byte[] dataHmacData = new byte[totalLength];
int offset = 0;
Array.Copy(data, 0x011, dataHmacData, offset, len1);
offset += len1;
Array.Copy(data, 0x0A0, dataHmacData, offset, len2);
offset += len2;
Array.Copy(calculatedTagHmac, 0, dataHmacData, offset, len3);
offset += len3;
Array.Copy(data, 0x000, dataHmacData, offset, len4);
offset += len4;
Array.Copy(data, 0x054, dataHmacData, offset, len5);
byte[] calculatedDataHmac;
using (var hmac = new HMACSHA256(hmacDataKey))
{
calculatedDataHmac = hmac.ComputeHash(dataHmacData);
}
byte[] storedDataHmac = new byte[32];
Array.Copy(data, 0x080, storedDataHmac, 0, 32);
if (!calculatedDataHmac.SequenceEqual(storedDataHmac))
{
throw new Exception("Data HMAC verification failed.");
}
}
public void Unlock()
{
if (!isLocked)
throw new InvalidOperationException("Data is already unlocked.");
// Derive keys and cipher
DeriveKeysAndCipher();
// Decrypt the encrypted data
DecryptData();
isLocked = false;
}
public void Lock()
{
if (isLocked)
throw new InvalidOperationException("Data is already locked.");
// Recalculate HMACs
DeriveHMACs();
// Encrypt the data
EncryptData();
isLocked = true;
}
public byte[] GetData()
{
return data;
}
// Property to get or set Amiibo nickname
public string AmiiboNickname
{
get
{
// data[0x020:0x034], big endian UTF-16
byte[] nicknameBytes = new byte[0x014];
Array.Copy(data, 0x020, nicknameBytes, 0, 0x014);
string nickname = System.Text.Encoding.BigEndianUnicode.GetString(nicknameBytes).TrimEnd('\0');
return nickname;
}
set
{
byte[] nicknameBytes = System.Text.Encoding.BigEndianUnicode.GetBytes(value.PadRight(10, '\0'));
if (nicknameBytes.Length > 20)
throw new ArgumentException("Nickname too long.");
Array.Copy(nicknameBytes, 0, data, 0x020, nicknameBytes.Length);
// Pad remaining bytes with zeros
for (int i = 0x020 + nicknameBytes.Length; i < 0x034; i++)
{
data[i] = 0x00;
}
}
}
}
}

View file

@ -0,0 +1,42 @@
using System;
using System.Linq;
namespace Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption
{
public class AmiiboMasterKey
{
public byte[] HmacKey { get; private set; } // 16 bytes
public byte[] TypeString { get; private set; } // 14 bytes
public byte Rfu { get; private set; } // 1 byte
public byte MagicSize { get; private set; } // 1 byte
public byte[] MagicBytes { get; private set; } // 16 bytes
public byte[] XorPad { get; private set; } // 32 bytes
public AmiiboMasterKey(byte[] data)
{
if (data.Length != 80)
throw new ArgumentException("Master key data must be 80 bytes.");
HmacKey = data.Take(16).ToArray();
TypeString = data.Skip(16).Take(14).ToArray();
Rfu = data[30];
MagicSize = data[31];
MagicBytes = data.Skip(32).Take(16).ToArray();
XorPad = data.Skip(48).Take(32).ToArray();
}
public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromCombinedBin(byte[] combinedBin)
{
if (combinedBin.Length != 160)
throw new ArgumentException($"Data is {combinedBin.Length} bytes (should be 160).");
byte[] dataBin = combinedBin.Take(80).ToArray();
byte[] tagBin = combinedBin.Skip(80).Take(80).ToArray();
AmiiboMasterKey dataKey = new AmiiboMasterKey(dataBin);
AmiiboMasterKey tagKey = new AmiiboMasterKey(tagBin);
return (dataKey, tagKey);
}
}
}

View file

@ -78,7 +78,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
if (_state == State.Initialized)
{
_cancelTokenSource?.Cancel();
// NOTE: All events are destroyed here.
context.Device.System.NfpDevices.Clear();
@ -146,9 +145,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
break;
}
}
_cancelTokenSource = new CancellationTokenSource();
Task.Run(() =>
{
while (true)
@ -199,7 +196,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
break;
}
}
return ResultCode.Success;
}
@ -229,7 +225,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
}
// TODO: Found how the MountTarget is handled.
for (int i = 0; i < context.Device.System.NfpDevices.Count; i++)
{
if (context.Device.System.NfpDevices[i].Handle == (PlayerIndex)deviceHandle)
@ -488,14 +483,12 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
#pragma warning disable IDE0059 // Remove unnecessary value assignment
uint deviceHandle = (uint)context.RequestData.ReadUInt64();
#pragma warning restore IDE0059
if (context.Device.System.NfpDevices.Count == 0)
{
return ResultCode.DeviceNotFound;
}
// NOTE: Since we handle amiibo through VirtualAmiibo, we don't have to flush anything in our case.
return ResultCode.Success;
}
@ -884,7 +877,6 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
return ResultCode.Success;
}
}
return ResultCode.DeviceNotFound;
}

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager
{
struct VirtualAmiiboFile
public struct VirtualAmiiboFile
{
public uint FileVersion { 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; }
}
struct VirtualAmiiboApplicationArea
public struct VirtualAmiiboApplicationArea
{
public uint ApplicationAreaId { get; set; }
public byte[] ApplicationArea { get; set; }

View file

@ -4,6 +4,7 @@ using Ryujinx.Common.Utilities;
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.Services.Mii;
using Ryujinx.HLE.HOS.Services.Mii.Types;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
using System;
using System.Collections.Generic;
@ -13,10 +14,11 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
static class VirtualAmiibo
{
private static uint _openedApplicationAreaId;
public static uint OpenedApplicationAreaId;
public static byte[] ApplicationBytes = new byte[0];
public static string InputBin = string.Empty;
public static string NickName = string.Empty;
private static readonly AmiiboJsonSerializerContext _serializerContext = AmiiboJsonSerializerContext.Default;
public static byte[] GenerateUuid(string amiiboId, bool useRandomUuid)
{
if (useRandomUuid)
@ -68,6 +70,11 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
VirtualAmiiboFile amiiboFile = LoadAmiiboFile(amiiboId);
string nickname = amiiboFile.NickName ?? "Ryujinx";
if (NickName != string.Empty)
{
nickname = NickName;
NickName = string.Empty;
}
UtilityImpl utilityImpl = new(tickSource);
CharInfo charInfo = new();
@ -97,16 +104,26 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
{
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
virtualAmiiboFile.NickName = newNickName;
if (InputBin != string.Empty)
{
AmiiboBinReader.SaveBinFile(InputBin, virtualAmiiboFile.NickName);
return;
}
SaveAmiiboFile(virtualAmiiboFile);
}
public static bool OpenApplicationArea(string amiiboId, uint applicationAreaId)
{
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
if (ApplicationBytes.Length > 0)
{
OpenedApplicationAreaId = applicationAreaId;
return true;
}
if (virtualAmiiboFile.ApplicationAreas.Exists(item => item.ApplicationAreaId == applicationAreaId))
{
_openedApplicationAreaId = applicationAreaId;
OpenedApplicationAreaId = applicationAreaId;
return true;
}
@ -116,11 +133,17 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
public static byte[] GetApplicationArea(string amiiboId)
{
if (ApplicationBytes.Length > 0)
{
byte[] bytes = ApplicationBytes;
ApplicationBytes = new byte[0];
return bytes;
}
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
foreach (VirtualAmiiboApplicationArea applicationArea in virtualAmiiboFile.ApplicationAreas)
{
if (applicationArea.ApplicationAreaId == _openedApplicationAreaId)
if (applicationArea.ApplicationAreaId == OpenedApplicationAreaId)
{
return applicationArea.ApplicationArea;
}
@ -151,17 +174,22 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
public static void SetApplicationArea(string amiiboId, byte[] applicationAreaData)
{
if (InputBin != string.Empty)
{
AmiiboBinReader.SaveBinFile(InputBin, applicationAreaData);
return;
}
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
if (virtualAmiiboFile.ApplicationAreas.Exists(item => item.ApplicationAreaId == _openedApplicationAreaId))
if (virtualAmiiboFile.ApplicationAreas.Exists(item => item.ApplicationAreaId == OpenedApplicationAreaId))
{
for (int i = 0; i < virtualAmiiboFile.ApplicationAreas.Count; i++)
{
if (virtualAmiiboFile.ApplicationAreas[i].ApplicationAreaId == _openedApplicationAreaId)
if (virtualAmiiboFile.ApplicationAreas[i].ApplicationAreaId == OpenedApplicationAreaId)
{
virtualAmiiboFile.ApplicationAreas[i] = new VirtualAmiiboApplicationArea()
{
ApplicationAreaId = _openedApplicationAreaId,
ApplicationAreaId = OpenedApplicationAreaId,
ApplicationArea = applicationAreaData,
};
@ -204,10 +232,21 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
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");
JsonHelper.SerializeToFile(filePath, virtualAmiiboFile, _serializerContext.VirtualAmiiboFile);
}
public static bool SaveFileExists(VirtualAmiiboFile virtualAmiiboFile)
{
if (InputBin != string.Empty)
{
SaveAmiiboFile(virtualAmiiboFile);
return true;
}
return File.Exists(Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", $"{virtualAmiiboFile.AmiiboId}.json"));
}
}
}

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_الإجراءات",
"MenuBarOptionsSimulateWakeUpMessage": "محاكاة رسالة الاستيقاظ",
"MenuBarActionsScanAmiibo": "‫فحص Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_الأدوات",
"MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت",
"MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Aktionen",
"MenuBarOptionsSimulateWakeUpMessage": "Aufwachnachricht simulieren",
"MenuBarActionsScanAmiibo": "Amiibo scannen",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Tools",
"MenuBarToolsInstallFirmware": "Firmware installieren",
"MenuBarFileToolsInstallFirmwareFromFile": "Firmware von einer XCI- oder einer ZIP-Datei installieren",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Δράσεις",
"MenuBarOptionsSimulateWakeUpMessage": "Προσομοίωση Μηνύματος Αφύπνισης",
"MenuBarActionsScanAmiibo": "Σάρωση Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Εργαλεία",
"MenuBarToolsInstallFirmware": "Εγκατάσταση Firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Εγκατάσταση Firmware από XCI ή ZIP",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Actions",
"MenuBarOptionsSimulateWakeUpMessage": "Simulate Wake-up message",
"MenuBarActionsScanAmiibo": "Scan An Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Tools",
"MenuBarToolsInstallFirmware": "Install Firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Acciones",
"MenuBarOptionsSimulateWakeUpMessage": "Simular mensaje de reactivación",
"MenuBarActionsScanAmiibo": "Escanear Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Herramientas",
"MenuBarToolsInstallFirmware": "Instalar firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware desde un archivo XCI o ZIP",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Actions",
"MenuBarOptionsSimulateWakeUpMessage": "Simuler un message de réveil",
"MenuBarActionsScanAmiibo": "Scanner un Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Outils",
"MenuBarToolsInstallFirmware": "Installer un firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Installer un firmware depuis un fichier XCI ou ZIP",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_פעולות",
"MenuBarOptionsSimulateWakeUpMessage": "דמה הודעת השכמה",
"MenuBarActionsScanAmiibo": "סרוק אמיבו",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_כלים",
"MenuBarToolsInstallFirmware": "התקן קושחה",
"MenuBarFileToolsInstallFirmwareFromFile": "התקן קושחה מקובץ- ZIP/XCI",

View file

@ -24,6 +24,7 @@
"MenuBarActions": "_Azioni",
"MenuBarOptionsSimulateWakeUpMessage": "Simula messaggio Wake-up",
"MenuBarActionsScanAmiibo": "Scansiona un Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Strumenti",
"MenuBarToolsInstallFirmware": "Installa firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Installa un firmware da file XCI o ZIP",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "アクション(_A)",
"MenuBarOptionsSimulateWakeUpMessage": "スリープ復帰メッセージをシミュレート",
"MenuBarActionsScanAmiibo": "Amiibo をスキャン",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "ツール(_T)",
"MenuBarToolsInstallFirmware": "ファームウェアをインストール",
"MenuBarFileToolsInstallFirmwareFromFile": "XCI または ZIP からファームウェアをインストール",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "동작(_A)",
"MenuBarOptionsSimulateWakeUpMessage": "웨이크업 메시지 시뮬레이션",
"MenuBarActionsScanAmiibo": "Amiibo 스캔",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "도구(_T)",
"MenuBarToolsInstallFirmware": "펌웨어 설치",
"MenuBarFileToolsInstallFirmwareFromFile": "XCI 또는 ZIP으로 펌웨어 설치",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Akcje",
"MenuBarOptionsSimulateWakeUpMessage": "Symuluj wiadomość wybudzania",
"MenuBarActionsScanAmiibo": "Skanuj Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Narzędzia",
"MenuBarToolsInstallFirmware": "Zainstaluj oprogramowanie",
"MenuBarFileToolsInstallFirmwareFromFile": "Zainstaluj oprogramowanie z XCI lub ZIP",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Ações",
"MenuBarOptionsSimulateWakeUpMessage": "_Simular mensagem de acordar console",
"MenuBarActionsScanAmiibo": "Escanear um Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Ferramentas",
"MenuBarToolsInstallFirmware": "_Instalar firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware a partir de um arquivo ZIP/XCI",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Действия",
"MenuBarOptionsSimulateWakeUpMessage": "Имитировать сообщение пробуждения",
"MenuBarActionsScanAmiibo": "Сканировать Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Инструменты",
"MenuBarToolsInstallFirmware": "Установка прошивки",
"MenuBarFileToolsInstallFirmwareFromFile": "Установить прошивку из XCI или ZIP",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "การดำเนินการ",
"MenuBarOptionsSimulateWakeUpMessage": "จำลองข้อความปลุก",
"MenuBarActionsScanAmiibo": "สแกนหา Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_เครื่องมือ",
"MenuBarToolsInstallFirmware": "ติดตั้งเฟิร์มแวร์",
"MenuBarFileToolsInstallFirmwareFromFile": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Eylemler",
"MenuBarOptionsSimulateWakeUpMessage": "Uyandırma Mesajı Simüle Et",
"MenuBarActionsScanAmiibo": "Bir Amiibo Tara",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Araçlar",
"MenuBarToolsInstallFirmware": "Yazılım Yükle",
"MenuBarFileToolsInstallFirmwareFromFile": "XCI veya ZIP'ten Yazılım Yükle",

View file

@ -1,7 +1,7 @@
{
"Language": "Українська",
"MenuBarFileOpenApplet": "Відкрити аплет",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Аплет для редагування Mii",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Відкрити аплет Mii Editor в автономному режимі",
"SettingsTabInputDirectMouseAccess": "Прямий доступ мишею",
"SettingsTabSystemMemoryManagerMode": "Режим диспетчера пам’яті:",
@ -11,10 +11,10 @@
"SettingsTabSystemUseHypervisor": "Використовувати гіпервізор",
"MenuBarFile": "_Файл",
"MenuBarFileOpenFromFile": "_Завантажити програму з файлу",
"MenuBarFileOpenFromFileError": "No applications found in selected file.",
"MenuBarFileOpenFromFileError": "У вибраному файлі не знайдено жодних додатків.",
"MenuBarFileOpenUnpacked": "Завантажити _розпаковану гру",
"MenuBarFileLoadDlcFromFolder": "Load DLC From Folder",
"MenuBarFileLoadTitleUpdatesFromFolder": "Load Title Updates From Folder",
"MenuBarFileLoadDlcFromFolder": "Завантажити DLC з теки",
"MenuBarFileLoadTitleUpdatesFromFolder": "Завантажити оновлення заголовків з теки",
"MenuBarFileOpenEmuFolder": "Відкрити теку Ryujinx",
"MenuBarFileOpenLogsFolder": "Відкрити теку журналів змін",
"MenuBarFileExit": "_Вихід",
@ -37,9 +37,9 @@
"MenuBarToolsManageFileTypes": "Керувати типами файлів",
"MenuBarToolsInstallFileTypes": "Установити типи файлів",
"MenuBarToolsUninstallFileTypes": "Видалити типи файлів",
"MenuBarToolsXCITrimmer": "Trim XCI Files",
"MenuBarView": "_View",
"MenuBarViewWindow": "Window Size",
"MenuBarToolsXCITrimmer": "Обрізати XCI файли",
"MenuBarView": "_Вид",
"MenuBarViewWindow": "Розмір вікна",
"MenuBarViewWindow720": "720p",
"MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Допомога",
@ -89,11 +89,11 @@
"GameListContextMenuOpenModsDirectoryToolTip": "Відкриває каталог, який містить модифікації Додатків",
"GameListContextMenuOpenSdModsDirectory": "Відкрити каталог модифікацій Atmosphere",
"GameListContextMenuOpenSdModsDirectoryToolTip": "Відкриває альтернативний каталог SD-карти Atmosphere, що містить модифікації Додатків. Корисно для модифікацій, зроблених для реального обладнання.",
"GameListContextMenuTrimXCI": "Check and Trim XCI File",
"GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space",
"GameListContextMenuTrimXCI": "Перевірка та Нарізка XCI Файлів",
"GameListContextMenuTrimXCIToolTip": "Перевірка та Нарізка XCI Файлів для збереження місця на диску",
"StatusBarGamesLoaded": "{0}/{1} ігор завантажено",
"StatusBarSystemVersion": "Версія системи: {0}",
"StatusBarXCIFileTrimming": "Trimming XCI File '{0}'",
"StatusBarXCIFileTrimming": "Обрізано XCI Файлів '{0}'",
"LinuxVmMaxMapCountDialogTitle": "Виявлено низьку межу для відображення памʼяті",
"LinuxVmMaxMapCountDialogTextPrimary": "Бажаєте збільшити значення vm.max_map_count на {0}",
"LinuxVmMaxMapCountDialogTextSecondary": "Деякі ігри можуть спробувати створити більше відображень памʼяті, ніж дозволено наразі. Ryujinx завершить роботу, щойно цей ліміт буде перевищено.",
@ -107,15 +107,15 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Увімкнути розширену присутність Discord",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Перевіряти наявність оновлень під час запуску",
"SettingsTabGeneralShowConfirmExitDialog": "Показати діалогове вікно «Підтвердити вихід».",
"SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralShowTitleBar": "Show Title Bar (Requires restart)",
"SettingsTabGeneralRememberWindowState": "Запам'ятати Розмір/Позицію вікна",
"SettingsTabGeneralShowTitleBar": "Показувати рядок заголовка (Потрібен перезапуск)",
"SettingsTabGeneralHideCursor": "Сховати вказівник:",
"SettingsTabGeneralHideCursorNever": "Ніколи",
"SettingsTabGeneralHideCursorOnIdle": "Сховати у режимі очікування",
"SettingsTabGeneralHideCursorAlways": "Завжди",
"SettingsTabGeneralGameDirectories": "Тека ігор",
"SettingsTabGeneralAutoloadDirectories": "Autoload DLC/Updates Directories",
"SettingsTabGeneralAutoloadNote": "DLC and Updates which refer to missing files will be unloaded automatically",
"SettingsTabGeneralAutoloadDirectories": "Автозавантаження каталогів DLC/Оновлень",
"SettingsTabGeneralAutoloadNote": "DLC та Оновлення, які посилаються на відсутні файли, будуть автоматично вимкнуті.",
"SettingsTabGeneralAdd": "Додати",
"SettingsTabGeneralRemove": "Видалити",
"SettingsTabSystem": "Система",
@ -148,7 +148,18 @@
"SettingsTabSystemSystemLanguageTraditionalChinese": "Традиційна китайська",
"SettingsTabSystemSystemTimeZone": "Часовий пояс системи:",
"SettingsTabSystemSystemTime": "Час системи:",
"SettingsTabSystemEnableVsync": "Вертикальна синхронізація",
"SettingsTabSystemVSyncMode": "Вертикальна синхронізація (VSync):",
"SettingsTabSystemEnableCustomVSyncInterval": "Увімкнути користувацьку частоту оновлення (Експериментально)",
"SettingsTabSystemVSyncModeSwitch": "Switch",
"SettingsTabSystemVSyncModeUnbounded": "Безмежна",
"SettingsTabSystemVSyncModeCustom": "Користувацька",
"SettingsTabSystemVSyncModeTooltip": "Емульована вертикальна синхронізація. 'Switch' емулює частоту оновлення Switch 60 Гц. 'Безмежна' — частота оновлення не матиме обмежень.",
"SettingsTabSystemVSyncModeTooltipCustom": "Емульована вертикальна синхронізація. 'Switch' емулює частоту оновлення Switch 60 Гц. 'Безмежна' — частота оновлення не матиме обмежень. 'Користувацька' емулює вказану користувацьку частоту оновлення.",
"SettingsTabSystemEnableCustomVSyncIntervalTooltip": "Дозволяє користувачу вказати емульовану частоту оновлення. У деяких іграх це може прискорити або сповільнити логіку гри. У інших іграх це може дозволити обмежити FPS на певні кратні частоти оновлення або призвести до непередбачуваної поведінки. Це експериментальна функція, без гарантій того, як це вплине на ігровий процес. \n\nЗалиште ВИМКНЕНИМ, якщо не впевнені.",
"SettingsTabSystemCustomVSyncIntervalValueTooltip": "Цільове значення користувацької частоти оновлення.",
"SettingsTabSystemCustomVSyncIntervalSliderTooltip": "Користувацька частота оновлення, як відсоток від стандартної частоти оновлення Switch.",
"SettingsTabSystemCustomVSyncIntervalPercentage": "Користувацька частота оновлення %:",
"SettingsTabSystemCustomVSyncIntervalValue": "Значення користувацька частота оновлення:",
"SettingsTabSystemEnablePptc": "PPTC (профільований постійний кеш перекладу)",
"SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC",
"SettingsTabSystemEnableFsIntegrityChecks": "Перевірка цілісності FS",
@ -159,11 +170,11 @@
"SettingsTabSystemAudioBackendSDL2": "SDL2",
"SettingsTabSystemHacks": "Хитрощі",
"SettingsTabSystemHacksNote": " (може викликати нестабільність)",
"SettingsTabSystemDramSize": "Використовувати альтернативне розташування пам'яті (розробники)",
"SettingsTabSystemDramSize4GiB": "4GiB",
"SettingsTabSystemDramSize6GiB": "6GiB",
"SettingsTabSystemDramSize8GiB": "8GiB",
"SettingsTabSystemDramSize12GiB": "12GiB",
"SettingsTabSystemDramSize": "Використовувати альтернативне розташування пам'яті (для розробників)",
"SettingsTabSystemDramSize4GiB": "4Гб",
"SettingsTabSystemDramSize6GiB": "6Гб",
"SettingsTabSystemDramSize8GiB": "8Гб",
"SettingsTabSystemDramSize12GiB": "12Гб",
"SettingsTabSystemIgnoreMissingServices": "Ігнорувати відсутні служби",
"SettingsTabSystemIgnoreApplet": "Ігнорувати Аплет",
"SettingsTabGraphics": "Графіка",
@ -201,8 +212,8 @@
"SettingsTabLoggingEnableGuestLogs": "Увімкнути журнали гостей",
"SettingsTabLoggingEnableFsAccessLogs": "Увімкнути журнали доступу Fs",
"SettingsTabLoggingFsGlobalAccessLogMode": "Режим журналу глобального доступу Fs:",
"SettingsTabLoggingDeveloperOptions": "Параметри розробника (УВАГА: знизиться продуктивність)",
"SettingsTabLoggingDeveloperOptionsNote": "УВАГА: Знижує продуктивність",
"SettingsTabLoggingDeveloperOptions": "Параметри розробника (УВАГА: шкодить продуктивності!)",
"SettingsTabLoggingDeveloperOptionsNote": "УВАГА: Зміна параметрів нижче негативно впливає на продуктивність",
"SettingsTabLoggingGraphicsBackendLogLevel": "Рівень журналу графічного сервера:",
"SettingsTabLoggingGraphicsBackendLogLevelNone": "Немає",
"SettingsTabLoggingGraphicsBackendLogLevelError": "Помилка",
@ -291,29 +302,29 @@
"ControllerSettingsMotionGyroDeadzone": "Мертва зона гіроскопа:",
"ControllerSettingsSave": "Зберегти",
"ControllerSettingsClose": "Закрити",
"KeyUnknown": "Unknown",
"KeyShiftLeft": "Shift Left",
"KeyShiftRight": "Shift Right",
"KeyControlLeft": "Ctrl Left",
"KeyMacControlLeft": "⌃ Left",
"KeyControlRight": "Ctrl Right",
"KeyMacControlRight": "⌃ Right",
"KeyAltLeft": "Alt Left",
"KeyMacAltLeft": "⌥ Left",
"KeyAltRight": "Alt Right",
"KeyMacAltRight": "⌥ Right",
"KeyWinLeft": "⊞ Left",
"KeyMacWinLeft": "⌘ Left",
"KeyWinRight": "⊞ Right",
"KeyMacWinRight": "⌘ Right",
"KeyUnknown": "Невідома",
"KeyShiftLeft": "Shift Лівий",
"KeyShiftRight": "Shift Правий",
"KeyControlLeft": "Ctrl Лівий",
"KeyMacControlLeft": "⌃ Лівий",
"KeyControlRight": "Ctrl Правий",
"KeyMacControlRight": "⌃ Правий",
"KeyAltLeft": "Alt Лівий",
"KeyMacAltLeft": "⌥ Лівий",
"KeyAltRight": "Alt Правий",
"KeyMacAltRight": "⌥ Правий",
"KeyWinLeft": "⊞ Лівий",
"KeyMacWinLeft": "⌘ Лівий",
"KeyWinRight": "⊞ Правий",
"KeyMacWinRight": "⌘ Правий",
"KeyMenu": "Menu",
"KeyUp": "Up",
"KeyDown": "Down",
"KeyLeft": "Left",
"KeyRight": "Right",
"KeyLeft": "Вліво",
"KeyRight": "Вправо",
"KeyEnter": "Enter",
"KeyEscape": "Escape",
"KeySpace": "Space",
"KeySpace": "Пробіл",
"KeyTab": "Tab",
"KeyBackSpace": "Backspace",
"KeyInsert": "Insert",
@ -327,7 +338,7 @@
"KeyPrintScreen": "Print Screen",
"KeyPause": "Pause",
"KeyNumLock": "Num Lock",
"KeyClear": "Clear",
"KeyClear": "Очистити",
"KeyKeypad0": "Keypad 0",
"KeyKeypad1": "Keypad 1",
"KeyKeypad2": "Keypad 2",
@ -366,17 +377,17 @@
"KeyPeriod": ".",
"KeySlash": "/",
"KeyBackSlash": "\\",
"KeyUnbound": "Unbound",
"GamepadLeftStick": "L Stick Button",
"GamepadRightStick": "R Stick Button",
"GamepadLeftShoulder": "Left Shoulder",
"GamepadRightShoulder": "Right Shoulder",
"GamepadLeftTrigger": "Left Trigger",
"GamepadRightTrigger": "Right Trigger",
"GamepadDpadUp": "Up",
"GamepadDpadDown": "Down",
"GamepadDpadLeft": "Left",
"GamepadDpadRight": "Right",
"KeyUnbound": "Відв'язати",
"GamepadLeftStick": "L Кнопка Стіку",
"GamepadRightStick": "R Кнопка Стіку",
"GamepadLeftShoulder": "Лівий Бампер",
"GamepadRightShoulder": "Правий Бампер",
"GamepadLeftTrigger": "Лівий Тригер",
"GamepadRightTrigger": "Правий Тригер",
"GamepadDpadUp": "Вверх",
"GamepadDpadDown": "Вниз",
"GamepadDpadLeft": "Вліво",
"GamepadDpadRight": "Вправо",
"GamepadMinus": "-",
"GamepadPlus": "+",
"GamepadGuide": "Guide",
@ -386,12 +397,12 @@
"GamepadPaddle3": "Paddle 3",
"GamepadPaddle4": "Paddle 4",
"GamepadTouchpad": "Touchpad",
"GamepadSingleLeftTrigger0": "Left Trigger 0",
"GamepadSingleRightTrigger0": "Right Trigger 0",
"GamepadSingleLeftTrigger1": "Left Trigger 1",
"GamepadSingleRightTrigger1": "Right Trigger 1",
"StickLeft": "Left Stick",
"StickRight": "Right Stick",
"GamepadSingleLeftTrigger0": "Лівий Тригер 0",
"GamepadSingleRightTrigger0": "Правий Тригер 0",
"GamepadSingleLeftTrigger1": "Лівий Тригер 1",
"GamepadSingleRightTrigger1": "Правий Тригер 1",
"StickLeft": "Лівий Стік",
"StickRight": "Правий Стік",
"UserProfilesSelectedUserProfile": "Вибраний профіль користувача:",
"UserProfilesSaveProfileName": "Зберегти ім'я профілю",
"UserProfilesChangeProfileImage": "Змінити зображення профілю",
@ -408,8 +419,8 @@
"InputDialogTitle": "Діалог введення",
"InputDialogOk": "Гаразд",
"InputDialogCancel": "Скасувати",
"InputDialogCancelling": "Cancelling",
"InputDialogClose": "Close",
"InputDialogCancelling": "Скасування",
"InputDialogClose": "Закрити",
"InputDialogAddNewProfileTitle": "Виберіть ім'я профілю",
"InputDialogAddNewProfileHeader": "Будь ласка, введіть ім'я профілю",
"InputDialogAddNewProfileSubtext": "(Макс. довжина: {0})",
@ -417,7 +428,7 @@
"AvatarSetBackgroundColor": "Встановити колір фону",
"AvatarClose": "Закрити",
"ControllerSettingsLoadProfileToolTip": "Завантажити профіль",
"ControllerSettingsViewProfileToolTip": "View Profile",
"ControllerSettingsViewProfileToolTip": "Показати профіль",
"ControllerSettingsAddProfileToolTip": "Додати профіль",
"ControllerSettingsRemoveProfileToolTip": "Видалити профіль",
"ControllerSettingsSaveProfileToolTip": "Зберегти профіль",
@ -427,7 +438,7 @@
"GameListContextMenuToggleFavorite": "Перемкнути вибране",
"GameListContextMenuToggleFavoriteToolTip": "Перемкнути улюблений статус гри",
"SettingsTabGeneralTheme": "Тема:",
"SettingsTabGeneralThemeAuto": "Auto",
"SettingsTabGeneralThemeAuto": "Авто.",
"SettingsTabGeneralThemeDark": "Темна",
"SettingsTabGeneralThemeLight": "Світла",
"ControllerSettingsConfigureGeneral": "Налаштування",
@ -446,7 +457,7 @@
"DialogExitSubMessage": "Усі незбережені дані буде втрачено!",
"DialogMessageCreateSaveErrorMessage": "Під час створення вказаних даних збереження сталася помилка: {0}",
"DialogMessageFindSaveErrorMessage": "Під час пошуку вказаних даних збереження сталася помилка: {0}",
"FolderDialogExtractTitle": "Виберіть папку для видобування",
"FolderDialogExtractTitle": "Виберіть теку для видобування",
"DialogNcaExtractionMessage": "Видобування розділу {0} з {1}...",
"DialogNcaExtractionTitle": "Екстрактор розділів NCA",
"DialogNcaExtractionMainNcaNotFoundErrorMessage": "Помилка видобування. Основний NCA не був присутній у вибраному файлі.",
@ -461,7 +472,7 @@
"DialogUpdaterExtractionMessage": "Видобування оновлення...",
"DialogUpdaterRenamingMessage": "Перейменування оновлення...",
"DialogUpdaterAddingFilesMessage": "Додавання нового оновлення...",
"DialogUpdaterShowChangelogMessage": "Show Changelog",
"DialogUpdaterShowChangelogMessage": "Показати список змін",
"DialogUpdaterCompleteMessage": "Оновлення завершено!",
"DialogUpdaterRestartMessage": "Перезапустити Ryujinx зараз?",
"DialogUpdaterNoInternetMessage": "Ви не підключені до Інтернету!",
@ -510,12 +521,12 @@
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "Встановлення прошивки...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Версію системи {0} успішно встановлено.",
"DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
"DialogKeysInstallerKeysInstallTitle": "Install Keys",
"DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
"DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
"DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
"DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
"DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
"DialogKeysInstallerKeysInstallTitle": "Встановлення Ключів",
"DialogKeysInstallerKeysInstallMessage": "Новий файл Ключів буде встановлено",
"DialogKeysInstallerKeysInstallSubMessage": "\n\nЦе замінить собою поточні файли Ключів.",
"DialogKeysInstallerKeysInstallConfirmMessage": "\n\nВи хочете продовжити?",
"DialogKeysInstallerKeysInstallWaitMessage": "Встановлення Ключів...",
"DialogKeysInstallerKeysInstallSuccessMessage": "Нові ключі встановлено.",
"DialogUserProfileDeletionWarningMessage": "Якщо вибраний профіль буде видалено, інші профілі не відкриватимуться",
"DialogUserProfileDeletionConfirmMessage": "Ви хочете видалити вибраний профіль",
"DialogUserProfileUnsavedChangesTitle": "Увага — Незбережені зміни",
@ -550,14 +561,14 @@
"MenuBarOptionsPauseEmulation": "Пауза",
"MenuBarOptionsResumeEmulation": "Продовжити",
"AboutUrlTooltipMessage": "Натисніть, щоб відкрити сайт Ryujinx у браузері за замовчування.",
"AboutDisclaimerMessage": "Ryujinx жодним чином не пов’язано з Nintendo™,\nчи будь-яким із їхніх партнерів.",
"AboutDisclaimerMessage": "Ryujinx жодним чином не пов’язаний з Nintendo™,\nчи будь-яким із їхніх партнерів.",
"AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) використовується в нашій емуляції Amiibo.",
"AboutGithubUrlTooltipMessage": "Натисніть, щоб відкрити сторінку GitHub Ryujinx у браузері за замовчуванням.",
"AboutDiscordUrlTooltipMessage": "Натисніть, щоб відкрити запрошення на сервер Discord Ryujinx у браузері за замовчуванням.",
"AboutRyujinxAboutTitle": "Про програму:",
"AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.",
"AboutRyujinxAboutContent": "Ryujinx — це емулятор для Nintendo Switch™.\nОтримуйте всі останні новини в нашому Discord.\nРозробники, які хочуть зробити внесок, можуть дізнатися більше на нашому GitHub або в Discord.",
"AboutRyujinxMaintainersTitle": "Підтримується:",
"AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:",
"AboutRyujinxFormerMaintainersTitle": "Минулі розробники:",
"AboutRyujinxMaintainersContentTooltipMessage": "Натисніть, щоб відкрити сторінку співавторів у вашому браузері за замовчування.",
"AmiiboSeriesLabel": "Серія Amiibo",
"AmiiboCharacterLabel": "Персонаж",
@ -585,9 +596,9 @@
"AddGameDirBoxTooltip": "Введіть каталог ігор, щоб додати до списку",
"AddGameDirTooltip": "Додати каталог гри до списку",
"RemoveGameDirTooltip": "Видалити вибраний каталог гри",
"AddAutoloadDirBoxTooltip": "Enter an autoload directory to add to the list",
"AddAutoloadDirTooltip": "Add an autoload directory to the list",
"RemoveAutoloadDirTooltip": "Remove selected autoload directory",
"AddAutoloadDirBoxTooltip": "Введіть шлях автозавантаження для додавання до списку",
"AddAutoloadDirTooltip": "Додайте шлях автозавантаження для додавання до списку",
"RemoveAutoloadDirTooltip": "Видалити вибраний каталог автозавантаження",
"CustomThemeCheckTooltip": "Використовуйте користувацьку тему Avalonia для графічного інтерфейсу, щоб змінити вигляд меню емулятора",
"CustomThemePathTooltip": "Шлях до користувацької теми графічного інтерфейсу",
"CustomThemeBrowseTooltip": "Огляд користувацької теми графічного інтерфейсу",
@ -600,7 +611,7 @@
"TimeTooltip": "Змінити час системи",
"VSyncToggleTooltip": "Емульована вертикальна синхронізація консолі. По суті, обмежувач кадрів для більшості ігор; його вимкнення може призвести до того, що ігри працюватимуть на вищій швидкості, екрани завантаження триватимуть довше чи зупинятимуться.\n\nМожна перемикати в грі гарячою клавішею (За умовчанням F1). Якщо ви плануєте вимкнути функцію, рекомендуємо зробити це через гарячу клавішу.\n\nЗалиште увімкненим, якщо не впевнені.",
"PptcToggleTooltip": "Зберігає перекладені функції JIT, щоб їх не потрібно було перекладати кожного разу, коли гра завантажується.\n\nЗменшує заїкання та значно прискорює час завантаження після першого завантаження гри.\n\nЗалиште увімкненим, якщо не впевнені.",
"LowPowerPptcToggleTooltip": "Load the PPTC using a third of the amount of cores.",
"LowPowerPptcToggleTooltip": "Завантажувати PPTC використовуючи третину від кількості ядер.",
"FsIntegrityToggleTooltip": "Перевіряє наявність пошкоджених файлів під час завантаження гри, і якщо виявлено пошкоджені файли, показує помилку хешу в журналі.\n\nНе впливає на продуктивність і призначений для усунення несправностей.\n\nЗалиште увімкненим, якщо не впевнені.",
"AudioBackendTooltip": "Змінює серверну частину, яка використовується для відтворення аудіо.\n\nSDL2 є кращим, тоді як OpenAL і SoundIO використовуються як резервні варіанти. Dummy не матиме звуку.\n\nВстановіть SDL2, якщо не впевнені.",
"MemoryManagerTooltip": "Змінює спосіб відображення та доступу до гостьової пам’яті. Значно впливає на продуктивність емульованого ЦП.\n\nВстановіть «Неперевірений хост», якщо не впевнені.",
@ -633,10 +644,10 @@
"DebugLogTooltip": "Друкує повідомлення журналу налагодження на консолі.\n\nВикористовуйте це лише за спеціальною вказівкою співробітника, оскільки це ускладнить читання журналів і погіршить роботу емулятора.",
"LoadApplicationFileTooltip": "Відкриває файловий провідник, щоб вибрати для завантаження сумісний файл Switch",
"LoadApplicationFolderTooltip": "Відкриває файловий провідник, щоб вибрати сумісну з комутатором розпаковану програму для завантаження",
"LoadDlcFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load DLC from",
"LoadTitleUpdatesFromFolderTooltip": "Open a file explorer to choose one or more folders to bulk load title updates from",
"OpenRyujinxFolderTooltip": "Відкриває папку файлової системи Ryujinx",
"OpenRyujinxLogsTooltip": "Відкриває папку, куди записуються журнали",
"LoadDlcFromFolderTooltip": "Відкрийте провідник файлів, щоб вибрати одну або кілька папок для масового завантаження DLC",
"LoadTitleUpdatesFromFolderTooltip": "Відкрийте провідник файлів, щоб вибрати одну або кілька папок для масового завантаження оновлень заголовків",
"OpenRyujinxFolderTooltip": "Відкриває теку файлової системи Ryujinx",
"OpenRyujinxLogsTooltip": "Відкриває теку, куди записуються журнали",
"ExitTooltip": "Виходить з Ryujinx",
"OpenSettingsTooltip": "Відкриває вікно налаштувань",
"OpenProfileManagerTooltip": "Відкриває вікно диспетчера профілів користувачів",
@ -664,7 +675,6 @@
"SettingsTabNetworkConnection": "Підключення до мережі",
"SettingsTabCpuCache": "Кеш ЦП",
"SettingsTabCpuMemory": "Пам'ять ЦП",
"DialogUpdaterFlatpakNotSupportedMessage": "Будь ласка, оновіть Ryujinx через FlatHub.",
"UpdaterDisabledWarningTitle": "Програму оновлення вимкнено!",
"ControllerSettingsRotate90": "Повернути на 90° за годинниковою стрілкою",
"IconSize": "Розмір значка",
@ -686,14 +696,14 @@
"OpenSetupGuideMessage": "Відкрити посібник із налаштування",
"NoUpdate": "Немає оновлень",
"TitleUpdateVersionLabel": "Версія {0} - {1}",
"TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
"TitleBundledDlcLabel": "Bundled:",
"TitleXCIStatusPartialLabel": "Partial",
"TitleXCIStatusTrimmableLabel": "Untrimmed",
"TitleXCIStatusUntrimmableLabel": "Trimmed",
"TitleXCIStatusFailedLabel": "(Failed)",
"TitleXCICanSaveLabel": "Save {0:n0} Mb",
"TitleXCISavingLabel": "Saved {0:n0} Mb",
"TitleBundledUpdateVersionLabel": "Комплектні: Версія {0}",
"TitleBundledDlcLabel": "Комплектні:",
"TitleXCIStatusPartialLabel": "Часткові",
"TitleXCIStatusTrimmableLabel": "Необрізані",
"TitleXCIStatusUntrimmableLabel": "Обрізані",
"TitleXCIStatusFailedLabel": "(Невдача)",
"TitleXCICanSaveLabel": "Зберегти {0:n0} Мб",
"TitleXCISavingLabel": "Збережено {0:n0} Мб",
"RyujinxInfo": "Ryujin x - Інформація",
"RyujinxConfirm": "Ryujinx - Підтвердження",
"FileDialogAllTypes": "Всі типи",
@ -701,8 +711,8 @@
"SwkbdMinCharacters": "Мінімальна кількість символів: {0}",
"SwkbdMinRangeCharacters": "Має бути {0}-{1} символів",
"CabinetTitle": "Cabinet Dialog",
"CabinetDialog": "Enter your Amiibo's new name",
"CabinetScanDialog": "Please scan your Amiibo now.",
"CabinetDialog": "Вкажіть Ваше нове ім'я Amiibo",
"CabinetScanDialog": "Будь ласка, проскануйте Ваш Amiibo.",
"SoftwareKeyboard": "Програмна клавіатура",
"SoftwareKeyboardModeNumeric": "Повинно бути лише 0-9 або “.”",
"SoftwareKeyboardModeAlphabet": "Повинно бути лише не CJK-символи",
@ -730,12 +740,15 @@
"AllSupportedFormats": "Усі підтримувані формати",
"RyujinxUpdater": "Програма оновлення Ryujinx",
"SettingsTabHotkeys": "Гарячі клавіші клавіатури",
"SettingsTabHotkeysToggleVSyncModeHotkey": "Перемкнути VSync режим:",
"SettingsTabHotkeysHotkeys": "Гарячі клавіші клавіатури",
"SettingsTabHotkeysToggleVsyncHotkey": "Увімк/вимк вертикальну синхронізацію:",
"SettingsTabHotkeysScreenshotHotkey": "Знімок екрана:",
"SettingsTabHotkeysShowUiHotkey": "Показати інтерфейс:",
"SettingsTabHotkeysPauseHotkey": "Пауза:",
"SettingsTabHotkeysToggleMuteHotkey": "Вимкнути звук:",
"SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey": "Підвищити користувацьку частоту оновлення",
"SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey": "Понизити користувацьку частоту оновлення",
"ControllerMotionTitle": "Налаштування керування рухом",
"ControllerRumbleTitle": "Налаштування вібрації",
"SettingsSelectThemeFileDialogTitle": "Виберіть файл теми",
@ -748,53 +761,53 @@
"SelectDlcDialogTitle": "Виберіть файли DLC",
"SelectUpdateDialogTitle": "Виберіть файли оновлення",
"SelectModDialogTitle": "Виберіть теку з модами",
"TrimXCIFileDialogTitle": "Check and Trim XCI File",
"TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.",
"TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB",
"TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details",
"TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details",
"TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details",
"TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.",
"TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim",
"TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details",
"TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details",
"TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed",
"TrimXCIFileCancelled": "The operation was cancelled",
"TrimXCIFileFileUndertermined": "No operation was performed",
"TrimXCIFileDialogTitle": "Перевірити та Обрізати XCI файл",
"TrimXCIFileDialogPrimaryText": "Ця функція спочатку перевірить вільний простір, а потім обрізатиме файл XCI для економії місця на диску.",
"TrimXCIFileDialogSecondaryText": "Поточний розмір файла: {0:n} MB\nРозмір файлів гри: {1:n} MB\nЕкономія місця: {2:n} MB",
"TrimXCIFileNoTrimNecessary": "XCI файл не потребує обрізання. Перевірте журнали для додаткової інформації",
"TrimXCIFileNoUntrimPossible": "XCI файл не може бути обрізаний. Перевірте журнали для додаткової інформації",
"TrimXCIFileReadOnlyFileCannotFix": "XCI файл Тільки для Читання і не може бути прочитаним. Перевірте журнали додаткової інформації",
"TrimXCIFileFileSizeChanged": "Розмір файлу XCI змінився з моменту сканування. Перевірте, чи не записується файл, та спробуйте знову",
"TrimXCIFileFreeSpaceCheckFailed": "Файл XCI містить дані в зоні вільного простору, тому обрізка небезпечна",
"TrimXCIFileInvalidXCIFile": "XCI Файл містить недійсні дані. Перевірте журнали для додаткової інформації",
"TrimXCIFileFileIOWriteError": "XCI Файл файл не вдалося відкрити для запису. Перевірте журнали для додаткової інформації",
"TrimXCIFileFailedPrimaryText": "Не вдалося обрізати файл XCI",
"TrimXCIFileCancelled": "Операція перервана",
"TrimXCIFileFileUndertermined": "Операція не проводилася",
"UserProfileWindowTitle": "Менеджер профілів користувачів",
"CheatWindowTitle": "Менеджер читів",
"DlcWindowTitle": "Менеджер вмісту для завантаження",
"ModWindowTitle": "Керувати модами для {0} ({1})",
"UpdateWindowTitle": "Менеджер оновлення назв",
"XCITrimmerWindowTitle": "XCI File Trimmer",
"XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected",
"XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)",
"XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...",
"XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...",
"XCITrimmerTitleStatusFailed": "Failed",
"XCITrimmerPotentialSavings": "Potential Savings",
"XCITrimmerActualSavings": "Actual Savings",
"XCITrimmerSavingsMb": "{0:n0} Mb",
"XCITrimmerSelectDisplayed": "Select Shown",
"XCITrimmerDeselectDisplayed": "Deselect Shown",
"XCITrimmerSortName": "Title",
"XCITrimmerSortSaved": "Space Savings",
"XCITrimmerTrim": "Trim",
"XCITrimmerUntrim": "Untrim",
"UpdateWindowUpdateAddedMessage": "{0} new update(s) added",
"UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.",
"XCITrimmerWindowTitle": "Обрізка XCI Файлів",
"XCITrimmerTitleStatusCount": "{0} з {1} тайтл(ів) обрано",
"XCITrimmerTitleStatusCountWithFilter": "{0} з {1} тайтл(ів) обрано ({2} відображається)",
"XCITrimmerTitleStatusTrimming": "Обрізка {0} тайтл(ів)...",
"XCITrimmerTitleStatusUntrimming": "Необрізаних {0} тайтл(ів)...",
"XCITrimmerTitleStatusFailed": "Невдача",
"XCITrimmerPotentialSavings": "Потенційна економія",
"XCITrimmerActualSavings": "Зекономлено",
"XCITrimmerSavingsMb": "{0:n0} Мб",
"XCITrimmerSelectDisplayed": "Вибрати показане",
"XCITrimmerDeselectDisplayed": "Скасувати вибір показаного",
"XCITrimmerSortName": "Заголовок",
"XCITrimmerSortSaved": "Економія місця",
"XCITrimmerTrim": "Обрізка",
"XCITrimmerUntrim": "Зшивання",
"UpdateWindowUpdateAddedMessage": "{0} нове оновлення додано",
"UpdateWindowBundledContentNotice": "Вбудовані оновлення не можуть бути видалені, лише вимкнені.",
"CheatWindowHeading": "Коди доступні для {0} [{1}]",
"BuildId": "ID збірки:",
"DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.",
"DlcWindowHeading": "{0} DLC(s) available",
"DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed",
"AutoloadUpdateAddedMessage": "{0} new update(s) added",
"AutoloadUpdateRemovedMessage": "{0} missing update(s) removed",
"DlcWindowBundledContentNotice": "Вбудований DLC не може бути видаленим, лише вимкненим.",
"DlcWindowHeading": "{0} DLC доступно",
"DlcWindowDlcAddedMessage": "{0} нового завантажувального вмісту додано",
"AutoloadDlcAddedMessage": "{0} нового завантажувального вмісту додано",
"AutoloadDlcRemovedMessage": "{0} відсутнього завантажувального вмісту видалено",
"AutoloadUpdateAddedMessage": "{0} нових оновлень додано",
"AutoloadUpdateRemovedMessage": "{0} відсутніх оновлень видалено",
"ModWindowHeading": "{0} мод(ів)",
"UserProfilesEditProfile": "Редагувати вибране",
"Continue": "Continue",
"Continue": "Продовжити",
"Cancel": "Скасувати",
"Save": "Зберегти",
"Discard": "Скасувати",
@ -832,7 +845,7 @@
"Name": "Назва",
"Size": "Розмір",
"Search": "Пошук",
"UserProfilesRecoverLostAccounts": "Відновлення втрачених облікових записів",
"UserProfilesRecoverLostAccounts": "Відновлення профілів",
"Recover": "Відновити",
"UserProfilesRecoverHeading": "Знайдено збереження для наступних облікових записів",
"UserProfilesRecoverEmptyList": "Немає профілів для відновлення",
@ -864,15 +877,15 @@
"MultiplayerModeDisabled": "Вимкнено",
"MultiplayerModeLdnMitm": "ldn_mitm",
"MultiplayerModeLdnRyu": "RyuLDN",
"MultiplayerDisableP2P": "Disable P2P Network Hosting (may increase latency)",
"MultiplayerDisableP2PTooltip": "Disable P2P network hosting, peers will proxy through the master server instead of connecting to you directly.",
"LdnPassphrase": "Network Passphrase:",
"LdnPassphraseTooltip": "You will only be able to see hosted games with the same passphrase as you.",
"LdnPassphraseInputTooltip": "Enter a passphrase in the format Ryujinx-<8 hex chars>. You will only be able to see hosted games with the same passphrase as you.",
"LdnPassphraseInputPublic": "(public)",
"GenLdnPass": "Generate Random",
"GenLdnPassTooltip": "Generates a new passphrase, which can be shared with other players.",
"ClearLdnPass": "Clear",
"ClearLdnPassTooltip": "Clears the current passphrase, returning to the public network.",
"InvalidLdnPassphrase": "Invalid Passphrase! Must be in the format \"Ryujinx-<8 hex chars>\""
"MultiplayerDisableP2P": "Вимкнути хостинг P2P мережі (може збільшити затримку)",
"MultiplayerDisableP2PTooltip": "Вимкнути хостинг P2P мережі, піри будуть підключатися через майстер-сервер замість прямого з'єднання з вами.",
"LdnPassphrase": "Мережевий пароль:",
"LdnPassphraseTooltip": "Ви зможете бачити лише ті ігри, які мають такий самий пароль, як і у вас.",
"LdnPassphraseInputTooltip": "Введіть пароль у форматі Ryujinx-<8 символів>. Ви зможете бачити лише ті ігри, які мають такий самий пароль, як і у вас.",
"LdnPassphraseInputPublic": "(публічний)",
"GenLdnPass": "Згенерувати випадкову",
"GenLdnPassTooltip": "Генерує новий пароль, яким можна поділитися з іншими гравцями.",
"ClearLdnPass": "Очистити",
"ClearLdnPassTooltip": "Очищає поточну пароль, повертаючись до публічної мережі.",
"InvalidLdnPassphrase": "Невірний пароль! Має бути в форматі \"Ryujinx-<8 символів>\""
}

View file

@ -27,6 +27,7 @@
"MenuBarActions": "操作(_A)",
"MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息",
"MenuBarActionsScanAmiibo": "扫描 Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "工具(_T)",
"MenuBarToolsInstallFirmware": "安装系统固件",
"MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 文件中安装系统固件",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "動作(_A)",
"MenuBarOptionsSimulateWakeUpMessage": "模擬喚醒訊息",
"MenuBarActionsScanAmiibo": "掃描 Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "工具(_T)",
"MenuBarToolsInstallFirmware": "安裝韌體",
"MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體",

View file

@ -29,12 +29,14 @@ using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.UI;
using Ryujinx.Input.HLE;
using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using Silk.NET.Vulkan;
using SkiaSharp;
using System;
using System.Collections.Generic;
@ -71,6 +73,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private string _gpuStatusText;
private string _shaderCountText;
private bool _isAmiiboRequested;
private bool _isAmiiboBinRequested;
private bool _showShaderCompilationHint;
private bool _isGameRunning;
private bool _isFullScreen;
@ -317,7 +320,16 @@ namespace Ryujinx.Ava.UI.ViewModels
OnPropertyChanged();
}
}
public bool IsAmiiboBinRequested
{
get => _isAmiiboBinRequested && _isGameRunning;
set
{
_isAmiiboBinRequested = value;
OnPropertyChanged();
}
}
public bool ShowLoadProgress
{
get => _showLoadProgress;
@ -2060,6 +2072,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()
{

View file

@ -241,6 +241,13 @@
Icon="{ext:Icon mdi-cube-scan}"
InputGesture="Ctrl + A"
IsEnabled="{Binding IsAmiiboRequested}" />
<MenuItem
Name="ScanAmiiboMenuItemFromBin"
AttachedToVisualTree="ScanBinAmiiboMenuItem_AttachedToVisualTree"
Click="OpenBinFile"
Header="{ext:Locale MenuBarActionsScanAmiiboBin}"
Icon="{ext:Icon mdi-cube-scan}"
IsEnabled="{Binding IsAmiiboBinRequested}" />
<MenuItem
Command="{Binding TakeScreenshot}"
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"

View file

@ -13,6 +13,7 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE;
using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common;
@ -151,6 +152,9 @@ namespace Ryujinx.Ava.UI.Views.Main
public async void OpenAmiiboWindow(object sender, RoutedEventArgs e)
=> await ViewModel.OpenAmiiboWindow();
public async void OpenBinFile(object sender, RoutedEventArgs e)
=> await ViewModel.OpenBinFile();
public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e)
{
if (!ViewModel.IsGameRunning)
@ -173,6 +177,12 @@ namespace Ryujinx.Ava.UI.Views.Main
ViewModel.IsAmiiboRequested = ViewModel.AppHost.Device.System.SearchingForAmiibo(out _);
}
private void ScanBinAmiiboMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is MenuItem)
ViewModel.IsAmiiboBinRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasKeyRetailBinPath();
}
private async void InstallFileTypes_Click(object sender, RoutedEventArgs e)
{
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();