Compare commits

...

8 commits

Author SHA1 Message Date
LotP1
fa44db8464
Merge e0acefeeef into 0adaa4cb96 2024-12-20 12:33:57 +00:00
LotP1
e0acefeeef default value of dishCacheSelector should be null 2024-12-20 13:33:46 +01:00
LotP1
b5604311cb Add nuke PPTC option to cache management 2024-12-20 13:33:46 +01:00
LotP1
2c53242b31 Remove outdated comment 2024-12-20 13:31:09 +01:00
LotP1
2874c40ee9 Implement blacklist functionality
- Added blacklist status to FunctionProfile
- Added PPTC Info file updater from v5518 and updated old updater logic to allow multiple update passes
- Added blacklist check to PPTC Cache loading
- Added marking functions as blacklisted if they do not yet exist at PPTC translation time
- Logger now shows how many functions were blacklisted when translating new functions to PPTC cache
2024-12-20 13:31:09 +01:00
LotP1
91518acf30 Fix incorrect hash logic
The stream hadn't been reset causing all hashes to be the same in most cases
2024-12-20 13:31:09 +01:00
LotP1
2af9a33979 Add cacheselector and allow PPTC with exefs mods
this is currently broken with Exlaunch mods that use hooks
2024-12-20 13:31:09 +01:00
Jacobwasbeast
0adaa4cb96
Adds the ability to read and write to amiibo bin files (#348)
Some checks are pending
Canary release job / Create tag (push) Waiting to run
Canary release job / Release for linux-arm64 (push) Waiting to run
Canary release job / Release for linux-x64 (push) Waiting to run
Canary release job / Release for win-x64 (push) Waiting to run
Canary release job / Release MacOS universal (push) Waiting to run
This introduces the ability to read and write game data and model
information from an Amiibo dump file (BIN format). Note that this
functionality requires the presence of a key_retail.bin file. For the
option to appear and function in the UI, ensure that the key_retail.bin
file is located in the <RyujinxData>/system folder.
2024-12-19 22:36:46 -06:00
39 changed files with 2129 additions and 941 deletions

View file

@ -3,6 +3,7 @@ using ARMeilleure.CodeGen.Linking;
using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.State;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
@ -30,7 +31,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 6997; //! To be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 7007; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@ -183,6 +184,36 @@ namespace ARMeilleure.Translation.PTC
InitializeCarriers();
}
private bool ContainsBlacklistedFunctions()
{
List<ulong> blacklist = Profiler.GetBlacklistedFunctions();
bool containsBlacklistedFunctions = false;
_infosStream.Seek(0L, SeekOrigin.Begin);
bool foundBadFunction = false;
for (int index = 0; index < GetEntriesCount(); index++)
{
InfoEntry infoEntry = DeserializeStructure<InfoEntry>(_infosStream);
foreach (ulong address in blacklist)
{
if (infoEntry.Address == address)
{
containsBlacklistedFunctions = true;
Logger.Warning?.Print(LogClass.Ptc, "PPTC cache invalidated: Found blacklisted functions in PPTC cache");
foundBadFunction = true;
break;
}
}
if (foundBadFunction)
{
break;
}
}
return containsBlacklistedFunctions;
}
private void PreLoad()
{
string fileNameActual = $"{CachePathActual}.cache";
@ -531,7 +562,7 @@ namespace ARMeilleure.Translation.PTC
public void LoadTranslations(Translator translator)
{
if (AreCarriersEmpty())
if (AreCarriersEmpty() || ContainsBlacklistedFunctions())
{
return;
}
@ -834,10 +865,18 @@ namespace ARMeilleure.Translation.PTC
while (profiledFuncsToTranslate.TryDequeue(out var item))
{
ulong address = item.address;
ExecutionMode executionMode = item.funcProfile.Mode;
bool highCq = item.funcProfile.HighCq;
Debug.Assert(Profiler.IsAddressInStaticCodeRange(address));
TranslatedFunction func = translator.Translate(address, item.funcProfile.Mode, item.funcProfile.HighCq);
TranslatedFunction func = translator.Translate(address, executionMode, highCq);
if (func == null)
{
Profiler.UpdateEntry(address, executionMode, true, true);
continue;
}
bool isAddressUnique = translator.Functions.TryAdd(address, func.GuestSize, func);
@ -884,7 +923,14 @@ namespace ARMeilleure.Translation.PTC
PtcStateChanged?.Invoke(PtcLoadingState.Loaded, _translateCount, _translateTotalCount);
Logger.Info?.Print(LogClass.Ptc, $"{_translateCount} of {_translateTotalCount} functions translated | Thread count: {degreeOfParallelism} in {sw.Elapsed.TotalSeconds} s");
if (_translateCount == _translateTotalCount)
{
Logger.Info?.Print(LogClass.Ptc, $"{_translateCount} of {_translateTotalCount} functions translated | Thread count: {degreeOfParallelism} in {sw.Elapsed.TotalSeconds} s");
}
else
{
Logger.Info?.Print(LogClass.Ptc, $"{_translateCount} of {_translateTotalCount} functions translated | {_translateTotalCount - _translateCount} function{(_translateTotalCount - _translateCount != 1 ? "s" : "")} blacklisted | Thread count: {degreeOfParallelism} in {sw.Elapsed.TotalSeconds} s");
}
Thread preSaveThread = new(PreSave)
{

View file

@ -23,10 +23,11 @@ namespace ARMeilleure.Translation.PTC
{
private const string OuterHeaderMagicString = "Pohd\0\0\0\0";
private const uint InternalVersion = 5518; //! Not to be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 7007; //! Not to be incremented manually for each change to the ARMeilleure project.
private static readonly uint[] _migrateInternalVersions = {
1866,
5518,
};
private const int SaveInterval = 30; // Seconds.
@ -72,20 +73,30 @@ namespace ARMeilleure.Translation.PTC
Enabled = false;
}
public void AddEntry(ulong address, ExecutionMode mode, bool highCq)
public void AddEntry(ulong address, ExecutionMode mode, bool highCq, bool blacklist = false)
{
if (IsAddressInStaticCodeRange(address))
{
Debug.Assert(!highCq);
lock (_lock)
if (blacklist)
{
ProfiledFuncs.TryAdd(address, new FuncProfile(mode, highCq: false));
lock (_lock)
{
ProfiledFuncs[address] = new FuncProfile(mode, highCq: false, true);
}
}
else
{
lock (_lock)
{
ProfiledFuncs.TryAdd(address, new FuncProfile(mode, highCq: false, false));
}
}
}
}
public void UpdateEntry(ulong address, ExecutionMode mode, bool highCq)
public void UpdateEntry(ulong address, ExecutionMode mode, bool highCq, bool? blacklist = null)
{
if (IsAddressInStaticCodeRange(address))
{
@ -95,7 +106,7 @@ namespace ARMeilleure.Translation.PTC
{
Debug.Assert(ProfiledFuncs.ContainsKey(address));
ProfiledFuncs[address] = new FuncProfile(mode, highCq: true);
ProfiledFuncs[address] = new FuncProfile(mode, highCq: true, blacklist ?? ProfiledFuncs[address].Blacklist);
}
}
}
@ -111,7 +122,7 @@ namespace ARMeilleure.Translation.PTC
foreach (var profiledFunc in ProfiledFuncs)
{
if (!funcs.ContainsKey(profiledFunc.Key))
if (!funcs.ContainsKey(profiledFunc.Key) && !profiledFunc.Value.Blacklist)
{
profiledFuncsToTranslate.Enqueue((profiledFunc.Key, profiledFunc.Value));
}
@ -126,6 +137,24 @@ namespace ARMeilleure.Translation.PTC
ProfiledFuncs.TrimExcess();
}
public List<ulong> GetBlacklistedFunctions()
{
List<ulong> funcs = new List<ulong>();
foreach (var profiledFunc in ProfiledFuncs)
{
if (profiledFunc.Value.Blacklist)
{
if (!funcs.Contains(profiledFunc.Key))
{
funcs.Add(profiledFunc.Key);
}
}
}
return funcs;
}
public void PreLoad()
{
_lastHash = default;
@ -216,13 +245,18 @@ namespace ARMeilleure.Translation.PTC
return false;
}
Func<ulong, FuncProfile, (ulong, FuncProfile)> migrateEntryFunc = null;
switch (outerHeader.InfoFileVersion)
{
case InternalVersion:
ProfiledFuncs = Deserialize(stream);
break;
case 1866:
ProfiledFuncs = Deserialize(stream, (address, profile) => (address + 0x500000UL, profile));
migrateEntryFunc = (address, profile) => (address + 0x500000UL, profile);
goto case 5518;
case 5518:
ProfiledFuncs = DeserializeAddBlacklist(stream, migrateEntryFunc);
break;
default:
Logger.Error?.Print(LogClass.Ptc, $"No migration path for {nameof(outerHeader.InfoFileVersion)} '{outerHeader.InfoFileVersion}'. Discarding cache.");
@ -252,6 +286,16 @@ namespace ARMeilleure.Translation.PTC
return DeserializeDictionary<ulong, FuncProfile>(stream, DeserializeStructure<FuncProfile>);
}
private static Dictionary<ulong, FuncProfile> DeserializeAddBlacklist(Stream stream, Func<ulong, FuncProfile, (ulong, FuncProfile)> migrateEntryFunc = null)
{
if (migrateEntryFunc != null)
{
return DeserializeAndUpdateDictionary(stream, (Stream stream) => { return new FuncProfile(DeserializeStructure<FuncProfilePreBlacklist>(stream)); }, migrateEntryFunc);
}
return DeserializeDictionary<ulong, FuncProfile>(stream, (Stream stream) => { return new FuncProfile(DeserializeStructure<FuncProfilePreBlacklist>(stream)); });
}
private static ReadOnlySpan<byte> GetReadOnlySpan(MemoryStream memoryStream)
{
return new(memoryStream.GetBuffer(), (int)memoryStream.Position, (int)memoryStream.Length - (int)memoryStream.Position);
@ -383,13 +427,35 @@ namespace ARMeilleure.Translation.PTC
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 5*/)]
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 6*/)]
public struct FuncProfile
{
public ExecutionMode Mode;
public bool HighCq;
public bool Blacklist;
public FuncProfile(ExecutionMode mode, bool highCq)
public FuncProfile(ExecutionMode mode, bool highCq, bool blacklist)
{
Mode = mode;
HighCq = highCq;
Blacklist = blacklist;
}
public FuncProfile(FuncProfilePreBlacklist fp)
{
Mode = fp.Mode;
HighCq = fp.HighCq;
Blacklist = false;
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 5*/)]
public struct FuncProfilePreBlacklist
{
public ExecutionMode Mode;
public bool HighCq;
public FuncProfilePreBlacklist(ExecutionMode mode, bool highCq)
{
Mode = mode;
HighCq = highCq;

View file

@ -249,6 +249,11 @@ namespace ARMeilleure.Translation
ControlFlowGraph cfg = EmitAndGetCFG(context, blocks, out Range funcRange, out Counter<uint> counter);
if (cfg == null)
{
return null;
}
ulong funcSize = funcRange.End - funcRange.Start;
Logger.EndPass(PassName.Translation, cfg);
@ -407,6 +412,11 @@ namespace ARMeilleure.Translation
if (opCode.Instruction.Emitter != null)
{
opCode.Instruction.Emitter(context);
if (opCode.Instruction.Name == InstName.Und && blkIndex == 0)
{
range = new Range(rangeStart, rangeEnd);
return null;
}
}
else
{

View file

@ -20,6 +20,7 @@ namespace Ryujinx.HLE.HOS
private readonly string _titleIdText;
private readonly string _displayVersion;
private readonly bool _diskCacheEnabled;
private readonly string _diskCacheSelector;
private readonly ulong _codeAddress;
private readonly ulong _codeSize;
@ -31,6 +32,7 @@ namespace Ryujinx.HLE.HOS
string titleIdText,
string displayVersion,
bool diskCacheEnabled,
string diskCacheSelector,
ulong codeAddress,
ulong codeSize)
{
@ -39,6 +41,7 @@ namespace Ryujinx.HLE.HOS
_titleIdText = titleIdText;
_displayVersion = displayVersion;
_diskCacheEnabled = diskCacheEnabled;
_diskCacheSelector = diskCacheSelector;
_codeAddress = codeAddress;
_codeSize = codeSize;
}
@ -114,7 +117,7 @@ namespace Ryujinx.HLE.HOS
}
}
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, "default"); //Ready for exefs profiles
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, _diskCacheSelector ?? "default");
return processContext;
}

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

@ -5,6 +5,7 @@ using LibHac.FsSystem;
using LibHac.Loader;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.RomFs;
using LibHac.Util;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
@ -18,6 +19,7 @@ using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using LazyFile = Ryujinx.HLE.HOS.Services.Fs.FileSystemProxy.LazyFile;
using Path = System.IO.Path;
@ -583,6 +585,7 @@ namespace Ryujinx.HLE.HOS
public BitVector32 Stubs;
public BitVector32 Replaces;
public MetaLoader Npdm;
public string Hash;
public bool Modified => (Stubs.Data | Replaces.Data) != 0;
}
@ -593,8 +596,11 @@ namespace Ryujinx.HLE.HOS
{
Stubs = new BitVector32(),
Replaces = new BitVector32(),
Hash = null,
};
string tempHash = string.Empty;
if (!_appMods.TryGetValue(applicationId, out ModCache mods) || mods.ExefsDirs.Count == 0)
{
return modLoadResult;
@ -630,8 +636,16 @@ namespace Ryujinx.HLE.HOS
modLoadResult.Replaces[1 << i] = true;
nsos[i] = new NsoExecutable(nsoFile.OpenRead().AsStorage(), nsoName);
Logger.Info?.Print(LogClass.ModLoader, $"NSO '{nsoName}' replaced");
using (FileStream stream = nsoFile.OpenRead())
{
nsos[i] = new NsoExecutable(stream.AsStorage(), nsoName);
Logger.Info?.Print(LogClass.ModLoader, $"NSO '{nsoName}' replaced");
using (MD5 md5 = MD5.Create())
{
stream.Seek(0, SeekOrigin.Begin);
tempHash += BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
}
}
}
modLoadResult.Stubs[1 << i] |= File.Exists(Path.Combine(mod.Path.FullName, nsoName + StubExtension));
@ -663,6 +677,14 @@ namespace Ryujinx.HLE.HOS
}
}
if (!string.IsNullOrEmpty(tempHash))
{
using (MD5 md5 = MD5.Create())
{
modLoadResult.Hash += BitConverter.ToString(md5.ComputeHash(tempHash.ToBytes())).Replace("-", "").ToLowerInvariant();
}
}
return modLoadResult;
}

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;
@ -14,10 +15,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)
@ -69,6 +71,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();
@ -98,16 +105,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.Any(item => item.ApplicationAreaId == applicationAreaId))
{
_openedApplicationAreaId = applicationAreaId;
OpenedApplicationAreaId = applicationAreaId;
return true;
}
@ -117,11 +134,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;
}
@ -152,17 +175,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.Any(item => item.ApplicationAreaId == _openedApplicationAreaId))
if (virtualAmiiboFile.ApplicationAreas.Any(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,
};
@ -205,10 +233,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

@ -82,13 +82,6 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
// Apply Nsos patches.
device.Configuration.VirtualFileSystem.ModLoader.ApplyNsoPatches(programId, nsoExecutables);
// Don't use PTC if ExeFS files have been replaced.
bool enablePtc = device.System.EnablePtc && !modLoadResult.Modified;
if (!enablePtc)
{
Logger.Warning?.Print(LogClass.Ptc, "Detected unsupported ExeFs modifications. PTC disabled.");
}
string programName = string.Empty;
if (!isHomebrew && programId > 0x010000000000FFFF)
@ -115,7 +108,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
device.System.KernelContext,
metaLoader,
nacpData,
enablePtc,
device.System.EnablePtc,
modLoadResult.Hash,
true,
programName,
metaLoader.GetProgramId(),

View file

@ -212,6 +212,7 @@ namespace Ryujinx.HLE.Loaders.Processes
dummyExeFs.GetNpdm(),
nacpData,
diskCacheEnabled: false,
diskCacheSelector: null,
allowCodeMemoryForJit: true,
programName,
programId,

View file

@ -186,6 +186,7 @@ namespace Ryujinx.HLE.Loaders.Processes
string.Empty,
string.Empty,
false,
null,
codeAddress,
codeSize);
@ -226,6 +227,7 @@ namespace Ryujinx.HLE.Loaders.Processes
MetaLoader metaLoader,
BlitStruct<ApplicationControlProperty> applicationControlProperties,
bool diskCacheEnabled,
string diskCacheSelector,
bool allowCodeMemoryForJit,
string name,
ulong programId,
@ -379,6 +381,7 @@ namespace Ryujinx.HLE.Loaders.Processes
$"{programId:x16}",
displayVersion,
diskCacheEnabled,
diskCacheSelector,
codeStart,
codeSize);

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_الإجراءات",
"MenuBarOptionsSimulateWakeUpMessage": "محاكاة رسالة الاستيقاظ",
"MenuBarActionsScanAmiibo": "‫فحص Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_الأدوات",
"MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت",
"MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "إدارة ذاكرة التخزين المؤقت",
"GameListContextMenuCacheManagementPurgePptc": "قائمة انتظار إعادة بناء الـPPTC",
"GameListContextMenuCacheManagementPurgePptcToolTip": "تنشيط PPTC لإعادة البناء في وقت الإقلاع عند بدء تشغيل اللعبة التالي",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "تنظيف ذاكرة مرشحات الفيديو المؤقتة",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "يحذف ذاكرة مرشحات الفيديو المؤقتة الخاصة بالتطبيق",
"GameListContextMenuCacheManagementOpenPptcDirectory": "‫فتح مجلد PPTC",
@ -504,6 +507,7 @@
"DialogWarning": "تحذير",
"DialogPPTCDeletionMessage": "أنت على وشك الإنتظار لإعادة بناء ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) عند الإقلاع التالي لـ:\n\n{0}\n\nأمتأكد من رغبتك في المتابعة؟",
"DialogPPTCDeletionErrorMessage": "خطأ خلال تنظيف ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) في {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "أنت على وشك حذف ذاكرة المظللات المؤقتة ل:\n\n{0}\n\nهل انت متأكد انك تريد المتابعة؟",
"DialogShaderDeletionErrorMessage": "حدث خطأ أثناء تنظيف ذاكرة المظللات المؤقتة في {0}: {1}",
"DialogRyujinxErrorMessage": "واجه ريوجينكس خطأ",

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",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "Cache-Verwaltung",
"GameListContextMenuCacheManagementPurgePptc": "PPTC als ungültig markieren",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Markiert den PPTC als ungültig, sodass dieser beim nächsten Spielstart neu erstellt wird",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "Shader Cache löschen",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Löscht den Shader-Cache der Anwendung",
"GameListContextMenuCacheManagementOpenPptcDirectory": "PPTC-Verzeichnis öffnen",
@ -504,6 +507,7 @@
"DialogWarning": "Warnung",
"DialogPPTCDeletionMessage": "Du bist dabei den PPTC für das folgende Spiel als ungültig zu markieren:\n\n{0}\n\nWirklich fortfahren?",
"DialogPPTCDeletionErrorMessage": "Fehler bei der Löschung des PPTC Caches bei {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "Du bist dabei, den Shader Cache zu löschen für :\n\n{0}\n\nWirklich fortfahren?",
"DialogShaderDeletionErrorMessage": "Es ist ein Fehler bei der Löschung des Shader Caches bei {0}: {1} aufgetreten",
"DialogRyujinxErrorMessage": "Ein Fehler ist aufgetreten",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Δράσεις",
"MenuBarOptionsSimulateWakeUpMessage": "Προσομοίωση Μηνύματος Αφύπνισης",
"MenuBarActionsScanAmiibo": "Σάρωση Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Εργαλεία",
"MenuBarToolsInstallFirmware": "Εγκατάσταση Firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Εγκατάσταση Firmware από XCI ή ZIP",
@ -76,6 +77,8 @@
"GameListContextMenuCacheManagementPurgePptc": "Εκκαθάριση Προσωρινής Μνήμης PPTC",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Διαγράφει την προσωρινή μνήμη PPTC της εφαρμογής",
"GameListContextMenuCacheManagementPurgeShaderCache": "Εκκαθάριση Προσωρινής Μνήμης Shader",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Διαγράφει την προσωρινή μνήμη Shader της εφαρμογής",
"GameListContextMenuCacheManagementOpenPptcDirectory": "Άνοιγμα Τοποθεσίας PPTC",
"GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "Ανοίγει την τοποθεσία που περιέχει τη προσωρινή μνήμη PPTC της εφαρμογής",
@ -504,6 +507,7 @@
"DialogWarning": "Προειδοποίηση",
"DialogPPTCDeletionMessage": "Πρόκειται να διαγράψετε την προσωρινή μνήμη PPTC για :\n\n{0}\n\nΕίστε βέβαιοι ότι θέλετε να συνεχίσετε;",
"DialogPPTCDeletionErrorMessage": "Σφάλμα κατά την εκκαθάριση προσωρινής μνήμης PPTC στο {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "Πρόκειται να διαγράψετε την προσωρινή μνήμη Shader για :\n\n{0}\n\nΕίστε βέβαιοι ότι θέλετε να συνεχίσετε;",
"DialogShaderDeletionErrorMessage": "Σφάλμα κατά την εκκαθάριση προσωρινής μνήμης Shader στο {0}: {1}",
"DialogRyujinxErrorMessage": "Το Ryujinx αντιμετώπισε σφάλμα",

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",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "Cache Management",
"GameListContextMenuCacheManagementPurgePptc": "Queue PPTC Rebuild",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Trigger PPTC to rebuild at boot time on the next game launch",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "Purge Shader Cache",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Deletes Application's shader cache",
"GameListContextMenuCacheManagementOpenPptcDirectory": "Open PPTC Directory",
@ -516,6 +519,7 @@
"DialogWarning": "Warning",
"DialogPPTCDeletionMessage": "You are about to queue a PPTC rebuild on the next boot of:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogPPTCDeletionErrorMessage": "Error purging PPTC cache at {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "You are about to delete the Shader cache for :\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionErrorMessage": "Error purging Shader cache at {0}: {1}",
"DialogRyujinxErrorMessage": "Ryujinx has encountered an error",

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",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "Gestión de caché ",
"GameListContextMenuCacheManagementPurgePptc": "Reconstruir PPTC en cola",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Elimina la caché de PPTC de esta aplicación",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "Limpiar caché de sombreadores",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Eliminar la caché de sombreadores de esta aplicación",
"GameListContextMenuCacheManagementOpenPptcDirectory": "Abrir carpeta de PPTC",
@ -504,6 +507,7 @@
"DialogWarning": "Advertencia",
"DialogPPTCDeletionMessage": "Vas a borrar la caché de PPTC para:\n\n{0}\n\n¿Estás seguro de querer continuar?",
"DialogPPTCDeletionErrorMessage": "Error purgando la caché de PPTC en {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "Vas a borrar la caché de sombreadores para:\n\n{0}\n\n¿Estás seguro de querer continuar?",
"DialogShaderDeletionErrorMessage": "Error purgando la caché de sombreadores en {0}: {1}",
"DialogRyujinxErrorMessage": "Ryujinx ha encontrado un error",

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",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "Gestion des caches",
"GameListContextMenuCacheManagementPurgePptc": "Reconstruction du PPTC",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Effectuer une reconstruction du PPTC au prochain démarrage du jeu",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "Purger les shaders",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Supprime les shaders du jeu",
"GameListContextMenuCacheManagementOpenPptcDirectory": "Ouvrir le dossier du PPTC",
@ -504,6 +507,7 @@
"DialogWarning": "Avertissement",
"DialogPPTCDeletionMessage": "Vous êtes sur le point de mettre en file d'attente une reconstruction PPTC au prochain démarrage de :\n\n{0}\n\nÊtes-vous sûr de vouloir continuer ?",
"DialogPPTCDeletionErrorMessage": "Erreur lors de la purge du cache PPTC à {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "Vous êtes sur le point de supprimer le cache du Shader pour :\n\n{0}\n\nÊtes-vous sûr de vouloir continuer ?",
"DialogShaderDeletionErrorMessage": "Erreur lors de la purge du cache du Shader à {0}: {1}",
"DialogRyujinxErrorMessage": "Ryujinx a rencontré une erreur",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_פעולות",
"MenuBarOptionsSimulateWakeUpMessage": "דמה הודעת השכמה",
"MenuBarActionsScanAmiibo": "סרוק אמיבו",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_כלים",
"MenuBarToolsInstallFirmware": "התקן קושחה",
"MenuBarFileToolsInstallFirmwareFromFile": "התקן קושחה מקובץ- ZIP/XCI",
@ -74,6 +75,8 @@
"GameListContextMenuManageDlcToolTip": "פותח את חלון מנהל הרחבות המשחקים",
"GameListContextMenuCacheManagement": "ניהול מטמון",
"GameListContextMenuCacheManagementPurgePptc": "הוסף PPTC לתור בנייה מחדש",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgePptcToolTip": "גרום ל-PPTC להבנות מחדש בפתיחה הבאה של המשחק",
"GameListContextMenuCacheManagementPurgeShaderCache": "ניקוי מטמון הצללות",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "מוחק את מטמון ההצללות של היישום",
@ -504,6 +507,7 @@
"DialogWarning": "אזהרה",
"DialogPPTCDeletionMessage": "אם תמשיכו אתם עומדים לגרום לבנייה מחדש של מטמון ה-PPTC עבור:\n\n{0}",
"DialogPPTCDeletionErrorMessage": "שגיאה בטיהור מטמון PPTC ב-{0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "אם תמשיכו אתם עומדים למחוק את מטמון ההצללות עבור:\n\n{0}",
"DialogShaderDeletionErrorMessage": "שגיאה בניקוי מטמון ההצללות ב-{0}: {1}",
"DialogRyujinxErrorMessage": "ריוג'ינקס נתקלה בשגיאה",

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",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "Gestione della cache",
"GameListContextMenuCacheManagementPurgePptc": "Accoda rigenerazione della cache PPTC",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Esegue la rigenerazione della cache PPTC al prossimo avvio del gioco",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "Elimina la cache degli shader",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Elimina la cache degli shader dell'applicazione",
"GameListContextMenuCacheManagementOpenPptcDirectory": "Apri la cartella della cache PPTC",
@ -504,6 +507,7 @@
"DialogWarning": "Avviso",
"DialogPPTCDeletionMessage": "Stai per accodare la rigenerazione della cache PPTC al prossimo avvio per:\n\n{0}\n\nSei sicuro di voler proseguire?",
"DialogPPTCDeletionErrorMessage": "Errore nell'eliminazione della cache PPTC a {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "Stai per eliminare la cache degli shader per:\n\n{0}\n\nSei sicuro di voler proseguire?",
"DialogShaderDeletionErrorMessage": "Errore nell'eliminazione della cache degli shader a {0}: {1}",
"DialogRyujinxErrorMessage": "Ryujinx ha incontrato un errore",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "アクション(_A)",
"MenuBarOptionsSimulateWakeUpMessage": "スリープ復帰メッセージをシミュレート",
"MenuBarActionsScanAmiibo": "Amiibo をスキャン",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "ツール(_T)",
"MenuBarToolsInstallFirmware": "ファームウェアをインストール",
"MenuBarFileToolsInstallFirmwareFromFile": "XCI または ZIP からファームウェアをインストール",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "キャッシュ管理",
"GameListContextMenuCacheManagementPurgePptc": "PPTC を再構築",
"GameListContextMenuCacheManagementPurgePptcToolTip": "次回のゲーム起動時に PPTC を再構築します",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "シェーダーキャッシュを破棄",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "アプリケーションのシェーダーキャッシュを破棄します",
"GameListContextMenuCacheManagementOpenPptcDirectory": "PPTC ディレクトリを開く",
@ -504,6 +507,7 @@
"DialogWarning": "警告",
"DialogPPTCDeletionMessage": "次回起動時に PPTC を再構築します:\n\n{0}\n\n実行してよろしいですか?",
"DialogPPTCDeletionErrorMessage": "PPTC キャッシュ破棄エラー {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "シェーダーキャッシュを破棄しようとしています:\n\n{0}\n\n実行してよろしいですか?",
"DialogShaderDeletionErrorMessage": "シェーダーキャッシュ破棄エラー {0}: {1}",
"DialogRyujinxErrorMessage": "エラーが発生しました",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "동작(_A)",
"MenuBarOptionsSimulateWakeUpMessage": "웨이크업 메시지 시뮬레이션",
"MenuBarActionsScanAmiibo": "Amiibo 스캔",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "도구(_T)",
"MenuBarToolsInstallFirmware": "펌웨어 설치",
"MenuBarFileToolsInstallFirmwareFromFile": "XCI 또는 ZIP으로 펌웨어 설치",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "캐시 관리",
"GameListContextMenuCacheManagementPurgePptc": "대기열 PPTC 재구성",
"GameListContextMenuCacheManagementPurgePptcToolTip": "다음 게임 실행 부팅 시, PPTC를 트리거하여 다시 구성",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "퍼지 셰이더 캐시",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "앱의 셰이더 캐시 삭제",
"GameListContextMenuCacheManagementOpenPptcDirectory": "PPTC 디렉터리 열기",
@ -504,6 +507,7 @@
"DialogWarning": "경고",
"DialogPPTCDeletionMessage": "다음에 부팅할 때, PPTC 재구축을 대기열에 추가하려고 합니다.\n\n{0}\n\n계속하시겠습니까?",
"DialogPPTCDeletionErrorMessage": "{0}에서 PPTC 캐시를 지우는 중 오류 발생 : {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "다음 셰이더 캐시를 삭제 :\n\n{0}\n\n계속하시겠습니까?",
"DialogShaderDeletionErrorMessage": "{0}에서 셰이더 캐시를 삭제하는 중 오류 발생 : {1}",
"DialogRyujinxErrorMessage": "Ryujinx에서 오류 발생",

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",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "Zarządzanie Cache",
"GameListContextMenuCacheManagementPurgePptc": "Zakolejkuj rekompilację PPTC",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Zainicjuj Rekompilację PPTC przy następnym uruchomieniu gry",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "Wyczyść pamięć podręczną cieni",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Usuwa pamięć podręczną cieni danej aplikacji",
"GameListContextMenuCacheManagementOpenPptcDirectory": "Otwórz katalog PPTC",
@ -504,6 +507,7 @@
"DialogWarning": "Uwaga",
"DialogPPTCDeletionMessage": "Masz zamiar umieścić w kolejce rekompilację PPTC przy następnym uruchomieniu:\n\n{0}\n\nCzy na pewno chcesz kontynuować?",
"DialogPPTCDeletionErrorMessage": "Błąd czyszczenia cache PPTC w {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "Zamierzasz usunąć cache Shaderów dla :\n\n{0}\n\nNa pewno chcesz kontynuować?",
"DialogShaderDeletionErrorMessage": "Błąd czyszczenia cache Shaderów w {0}: {1}",
"DialogRyujinxErrorMessage": "Ryujinx napotkał błąd",

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",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "Gerenciamento de cache",
"GameListContextMenuCacheManagementPurgePptc": "Limpar cache PPTC",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Deleta o cache PPTC armazenado em disco do jogo",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "Limpar cache de Shader",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Deleta o cache de Shader armazenado em disco do jogo",
"GameListContextMenuCacheManagementOpenPptcDirectory": "Abrir diretório do cache PPTC",
@ -504,6 +507,7 @@
"DialogWarning": "Alerta",
"DialogPPTCDeletionMessage": "Você está prestes a apagar o cache PPTC para :\n\n{0}\n\nTem certeza que deseja continuar?",
"DialogPPTCDeletionErrorMessage": "Erro apagando cache PPTC em {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "Você está prestes a apagar o cache de Shader para :\n\n{0}\n\nTem certeza que deseja continuar?",
"DialogShaderDeletionErrorMessage": "Erro apagando o cache de Shader em {0}: {1}",
"DialogRyujinxErrorMessage": "Ryujinx encontrou um erro",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "_Действия",
"MenuBarOptionsSimulateWakeUpMessage": "Имитировать сообщение пробуждения",
"MenuBarActionsScanAmiibo": "Сканировать Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_Инструменты",
"MenuBarToolsInstallFirmware": "Установка прошивки",
"MenuBarFileToolsInstallFirmwareFromFile": "Установить прошивку из XCI или ZIP",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "Управление кэшем",
"GameListContextMenuCacheManagementPurgePptc": "Перестроить очередь PPTC",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Запускает перестройку PPTC во время следующего запуска игры.",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "Очистить кэш шейдеров",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Удаляет кеш шейдеров приложения",
"GameListContextMenuCacheManagementOpenPptcDirectory": "Открыть папку PPTC",
@ -504,6 +507,7 @@
"DialogWarning": "Внимание",
"DialogPPTCDeletionMessage": "Вы собираетесь перестроить кэш PPTC при следующем запуске для:\n\n{0}\n\nВы уверены, что хотите продолжить?",
"DialogPPTCDeletionErrorMessage": "Ошибка очистки кэша PPTC в {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "Вы собираетесь удалить кэш шейдеров для:\n\n{0}\n\nВы уверены, что хотите продолжить?",
"DialogShaderDeletionErrorMessage": "Ошибка очистки кэша шейдеров в {0}: {1}",
"DialogRyujinxErrorMessage": "Ryujinx обнаружил ошибку",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "การดำเนินการ",
"MenuBarOptionsSimulateWakeUpMessage": "จำลองข้อความปลุก",
"MenuBarActionsScanAmiibo": "สแกนหา Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "_เครื่องมือ",
"MenuBarToolsInstallFirmware": "ติดตั้งเฟิร์มแวร์",
"MenuBarFileToolsInstallFirmwareFromFile": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "จัดการแคช",
"GameListContextMenuCacheManagementPurgePptc": "เพิ่มคิวการสร้าง PPTC ใหม่",
"GameListContextMenuCacheManagementPurgePptcToolTip": "ให้ PPTC สร้างใหม่ในเวลาบูตเมื่อเปิดเกมครั้งถัดไป",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "ล้างแคช แสงเงา",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "ลบแคช แสงเงา ของแอปพลิเคชัน",
"GameListContextMenuCacheManagementOpenPptcDirectory": "เปิดไดเรกทอรี่ PPTC",

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",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "Önbellek Yönetimi",
"GameListContextMenuCacheManagementPurgePptc": "PPTC Yeniden Yapılandırmasını Başlat",
"GameListContextMenuCacheManagementPurgePptcToolTip": "Oyunun bir sonraki açılışında PPTC'yi yeniden yapılandır",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "Shader Önbelleğini Temizle",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "Uygulamanın shader önbelleğini temizler",
"GameListContextMenuCacheManagementOpenPptcDirectory": "PPTC Dizinini Aç",
@ -504,6 +507,7 @@
"DialogWarning": "Uyarı",
"DialogPPTCDeletionMessage": "Belirtilen PPTC cache silinecek :\n\n{0}\n\nDevam etmek istediğinizden emin misiniz?",
"DialogPPTCDeletionErrorMessage": "Belirtilen PPTC cache temizlenirken hata {0}: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "Belirtilen Shader cache silinecek :\n\n{0}\n\nDevam etmek istediğinizden emin misiniz?",
"DialogShaderDeletionErrorMessage": "Belirtilen Shader cache temizlenirken hata {0}: {1}",
"DialogRyujinxErrorMessage": "Ryujinx bir hata ile karşılaştı",

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,7 @@
"MenuBarActions": "操作(_A)",
"MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息",
"MenuBarActionsScanAmiibo": "扫描 Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "工具(_T)",
"MenuBarToolsInstallFirmware": "安装系统固件",
"MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 文件中安装系统固件",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "缓存管理",
"GameListContextMenuCacheManagementPurgePptc": "清除 PPTC 缓存文件",
"GameListContextMenuCacheManagementPurgePptcToolTip": "删除游戏的 PPTC 缓存文件,下次启动游戏时重新编译生成 PPTC 缓存文件",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "清除着色器缓存文件",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "删除游戏的着色器缓存文件,下次启动游戏时重新生成着色器缓存文件",
"GameListContextMenuCacheManagementOpenPptcDirectory": "打开 PPTC 缓存目录",
@ -504,6 +507,7 @@
"DialogWarning": "警告",
"DialogPPTCDeletionMessage": "您即将删除:\n\n{0} 的 PPTC 缓存文件\n\n确定吗",
"DialogPPTCDeletionErrorMessage": "清除 {0} 的 PPTC 缓存文件时出错:{1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "您即将删除:\n\n{0} 的着色器缓存文件\n\n确定吗",
"DialogShaderDeletionErrorMessage": "清除 {0} 的着色器缓存文件时出错:{1}",
"DialogRyujinxErrorMessage": "Ryujinx 模拟器发生错误",

View file

@ -27,6 +27,7 @@
"MenuBarActions": "動作(_A)",
"MenuBarOptionsSimulateWakeUpMessage": "模擬喚醒訊息",
"MenuBarActionsScanAmiibo": "掃描 Amiibo",
"MenuBarActionsScanAmiiboBin": "Scan An Amiibo (From Bin)",
"MenuBarTools": "工具(_T)",
"MenuBarToolsInstallFirmware": "安裝韌體",
"MenuBarFileToolsInstallFirmwareFromFile": "從 XCI 或 ZIP 安裝韌體",
@ -75,6 +76,8 @@
"GameListContextMenuCacheManagement": "快取管理",
"GameListContextMenuCacheManagementPurgePptc": "佇列 PPTC 重建",
"GameListContextMenuCacheManagementPurgePptcToolTip": "下一次啟動遊戲時,觸發 PPTC 進行重建",
"GameListContextMenuCacheManagementNukePptc": "Purge PPTC cache",
"GameListContextMenuCacheManagementNukePptcToolTip": "Deletes Application's PPTC cache",
"GameListContextMenuCacheManagementPurgeShaderCache": "清除著色器快取",
"GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "刪除應用程式的著色器快取",
"GameListContextMenuCacheManagementOpenPptcDirectory": "開啟 PPTC 資料夾",
@ -504,6 +507,7 @@
"DialogWarning": "警告",
"DialogPPTCDeletionMessage": "您將在下一次啟動時佇列重建以下遊戲的 PPTC:\n\n{0}\n\n您確定要繼續嗎?",
"DialogPPTCDeletionErrorMessage": "在 {0} 清除 PPTC 快取時出錯: {1}",
"DialogPPTCNukeMessage": "You are about to purge all PPTC data from:\n\n{0}\n\nAre you sure you want to proceed?",
"DialogShaderDeletionMessage": "您將刪除以下遊戲的著色器快取:\n\n{0}\n\n您確定要繼續嗎?",
"DialogShaderDeletionErrorMessage": "在 {0} 清除著色器快取時出錯: {1}",
"DialogRyujinxErrorMessage": "Ryujinx 遇到錯誤",

View file

@ -82,6 +82,11 @@
Header="{ext:Locale GameListContextMenuCacheManagementPurgePptc}"
Icon="{ext:Icon mdi-refresh}"
ToolTip.Tip="{ext:Locale GameListContextMenuCacheManagementPurgePptcToolTip}" />
<MenuItem
Click="NukePtcCache_Click"
Header="{ext:Locale GameListContextMenuCacheManagementNukePptc}"
Icon="{ext:Icon mdi-delete-alert}"
ToolTip.Tip="{ext:Locale GameListContextMenuCacheManagementNukePptcToolTip}" />
<MenuItem
Click="PurgeShaderCache_Click"
Header="{ext:Locale GameListContextMenuCacheManagementPurgeShaderCache}"

View file

@ -170,6 +170,52 @@ namespace Ryujinx.Ava.UI.Controls
}
}
public async void NukePtcCache_Click(object sender, RoutedEventArgs args)
{
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return;
UserResult result = await ContentDialogHelper.CreateLocalizedConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogWarning],
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCNukeMessage, viewModel.SelectedApplication.Name)
);
if (result == UserResult.Yes)
{
DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0"));
DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1"));
List<FileInfo> cacheFiles = new();
if (mainDir.Exists)
{
cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache"));
cacheFiles.AddRange(mainDir.EnumerateFiles("*.info"));
}
if (backupDir.Exists)
{
cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache"));
cacheFiles.AddRange(mainDir.EnumerateFiles("*.info"));
}
if (cacheFiles.Count > 0)
{
foreach (FileInfo file in cacheFiles)
{
try
{
file.Delete();
}
catch (Exception ex)
{
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionErrorMessage, file.Name, ex));
}
}
}
}
}
public async void PurgeShaderCache_Click(object sender, RoutedEventArgs args)
{
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })

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();