From 6b155f5fbe00312b0d4ce785ba36e1cc9b12f99b Mon Sep 17 00:00:00 2001 From: Jacobwasbeast Date: Tue, 3 Dec 2024 21:50:33 -0600 Subject: [PATCH] First attempt Currently DateTime is incorrect, Still trying to figure out application data. --- src/Ryujinx.HLE/HOS/Horizon.cs | 12 + .../Nfc/AmiiboDecryption/AmiiboBinReader.cs | 233 ++++++++++++++++++ .../Nfc/AmiiboDecryption/AmiiboDecrypter.cs | 113 +++++++++ .../Nfc/AmiiboDecryption/AmiiboMasterKey.cs | 69 ++++++ .../Nfp/NfpManager/Types/VirtualAmiiboFile.cs | 4 +- .../HOS/Services/Nfc/Nfp/VirtualAmiibo.cs | 2 +- src/Ryujinx/Assets/Locales/en_US.json | 1 + .../UI/ViewModels/MainWindowViewModel.cs | 28 +++ .../UI/Views/Main/MainMenuBarView.axaml | 7 + .../UI/Views/Main/MainMenuBarView.axaml.cs | 3 + 10 files changed, 469 insertions(+), 3 deletions(-) create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboMasterKey.cs diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index 64b08e309..d961e1e6a 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -16,6 +16,7 @@ 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.Bin; using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; using Ryujinx.HLE.HOS.Services.Nv; using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl; @@ -344,6 +345,17 @@ namespace Ryujinx.HLE.HOS NfpDevices[nfpDeviceId].UseRandomUuid = useRandomUuid; } } + public void ScanAmiiboFromBin(string path) + { + byte[] encryptedData = File.ReadAllBytes(path); + VirtualAmiiboFile newFile = AmiiboBinReader.ReadBinFile(encryptedData); + if (SearchingForAmiibo(out int nfpDeviceId)) + { + NfpDevices[nfpDeviceId].State = NfpDeviceState.TagFound; + NfpDevices[nfpDeviceId].AmiiboId = newFile.AmiiboId; + NfpDevices[nfpDeviceId].UseRandomUuid = false; + } + } public bool SearchingForAmiibo(out int nfpDeviceId) { diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs new file mode 100644 index 000000000..8e32db885 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs @@ -0,0 +1,233 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; +using Ryujinx.HLE.HOS.Tamper; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Bin +{ + public class AmiiboBinReader + { + // Method to calculate BCC (XOR checksum) from UID bytes + private static byte CalculateBCC(byte[] uid, int startIdx) + { + return (byte)(uid[startIdx] ^ uid[startIdx + 1] ^ uid[startIdx + 2] ^ 0x88); + } + + // Method to read and process a .bin file + public static VirtualAmiiboFile ReadBinFile(byte[] fileBytes) + { + string keyRetailBinPath = GetKeyRetailBinPath(); + if (string.IsNullOrEmpty(keyRetailBinPath)) + { + Console.WriteLine("Key retail bin path not found."); + return new VirtualAmiiboFile(); + } + + byte[] initialCounter = new byte[16]; + + // Ensure the file is long enough + if (fileBytes.Length < 128 * 4) // Each page is 4 bytes, total 512 bytes + { + Console.WriteLine("File is too short to process."); + return new VirtualAmiiboFile(); + } + + // Decrypt the Amiibo data + AmiiboDecrypter amiiboDecryptor = new AmiiboDecrypter(keyRetailBinPath); + byte[] decryptedFileBytes = amiiboDecryptor.DecryptAmiiboData(fileBytes, initialCounter); + + // Assuming the UID is stored in the first 7 bytes (NTAG215 UID length) + byte[] uid = new byte[7]; + Array.Copy(fileBytes, 0, uid, 0, 7); + + // Calculate BCC values + byte bcc0 = CalculateBCC(uid, 0); // BCC0 = UID0 ^ UID1 ^ UID2 ^ 0x88 + byte bcc1 = CalculateBCC(uid, 3); // BCC1 = UID3 ^ UID4 ^ UID5 ^ 0x00 + + Console.WriteLine($"UID: {BitConverter.ToString(uid)}"); + Console.WriteLine($"BCC0: 0x{bcc0:X2}, BCC1: 0x{bcc1:X2}"); + + // Initialize byte arrays for data extraction + byte[] nickNameBytes = new byte[20]; // Amiibo nickname is 20 bytes + byte[] titleId = new byte[8]; + byte[] usedCharacter = new byte[2]; + byte[] variation = new byte[2]; + byte[] amiiboID = new byte[2]; + byte[] setID = new byte[1]; + byte[] initDate = new byte[2]; + byte[] writeDate = new byte[2]; + byte[] writeCounter = new byte[2]; + byte formData = 0; + byte[] applicationAreas = new byte[212]; + + // Reading specific pages and parsing bytes + for (int page = 0; page < 128; page++) // NTAG215 has 128 pages + { + int pageStartIdx = page * 4; // Each page is 4 bytes + byte[] pageData = new byte[4]; + bool isEncrypted = IsPageEncrypted(page); + byte[] sourceBytes = isEncrypted ? decryptedFileBytes : fileBytes; + Array.Copy(sourceBytes, pageStartIdx, pageData, 0, 4); + + // Special handling for specific pages + switch (page) + { + case 0: // Page 0 (UID + BCC0) + Console.WriteLine("Page 0: UID and BCC0."); + break; + + case 2: // Page 2 (BCC1 + Internal Value) + byte internalValue = pageData[1]; + Console.WriteLine($"Page 2: BCC1 + Internal Value 0x{internalValue:X2} (Expected 0x48)."); + break; + + case 6: + // Bytes 0 and 1 are init date, bytes 2 and 3 are write date + Array.Copy(pageData, 0, initDate, 0, 2); + Array.Copy(pageData, 2, writeDate, 0, 2); + break; + + case 8: + case 9: + case 10: + case 11: + case 12: + // Extract nickname bytes + int nickNameOffset = (page - 8) * 4; + Array.Copy(pageData, 0, nickNameBytes, nickNameOffset, 4); + break; + + case 21: + // Bytes 0 and 1 are used character, bytes 2 and 3 are variation + Array.Copy(pageData, 0, usedCharacter, 0, 2); + Array.Copy(pageData, 2, variation, 0, 2); + break; + + case 22: + // Bytes 0 and 1 are amiibo ID, byte 2 is set ID, byte 3 is form data + Array.Copy(pageData, 0, amiiboID, 0, 2); + setID[0] = pageData[2]; + formData = pageData[3]; + break; + + case 64: + case 65: + // Extract title ID + int titleIdOffset = (page - 64) * 4; + Array.Copy(pageData, 0, titleId, titleIdOffset, 4); + break; + + case 66: + // Bytes 0 and 1 are write counter + Array.Copy(pageData, 0, writeCounter, 0, 2); + break; + + // Pages 76 to 127 are application areas + case >= 76 and <= 127: + int appAreaOffset = (page - 76) * 4; + Array.Copy(pageData, 0, applicationAreas, appAreaOffset, 4); + break; + } + } + // Debugging + string titleIdStr = BitConverter.ToString(titleId).Replace("-", ""); + string usedCharacterStr = BitConverter.ToString(usedCharacter).Replace("-", ""); + string variationStr = BitConverter.ToString(variation).Replace("-", ""); + string amiiboIDStr = BitConverter.ToString(amiiboID).Replace("-", ""); + string formDataStr = formData.ToString("X2"); + string setIDStr = BitConverter.ToString(setID).Replace("-", ""); + string nickName = Encoding.BigEndianUnicode.GetString(nickNameBytes).TrimEnd('\0'); + string head = usedCharacterStr + variationStr; + string tail = amiiboIDStr + setIDStr + "02"; + string finalID = head + tail; + string initDateStr = BitConverter.ToString(initDate).Replace("-", ""); + string writeDateStr = BitConverter.ToString(writeDate).Replace("-", ""); + + Console.WriteLine($"Title ID: {titleIdStr}"); + Console.WriteLine($"Head: {head}"); + Console.WriteLine($"Tail: {tail}"); + Console.WriteLine($"Used Character: {usedCharacterStr}"); + Console.WriteLine($"Form Data: {formDataStr}"); + Console.WriteLine($"Variation: {variationStr}"); + Console.WriteLine($"Amiibo ID: {amiiboIDStr}"); + Console.WriteLine($"Set ID: {setIDStr}"); + Console.WriteLine($"Final ID: {finalID}"); + Console.WriteLine($"Nickname: {nickName}"); + Console.WriteLine($"Init Date: {initDateStr}"); + Console.WriteLine($"Write Date: {writeDateStr}"); + + VirtualAmiiboFile virtualAmiiboFile = new VirtualAmiiboFile + { + FileVersion = 1, + TagUuid = uid, + AmiiboId = finalID + }; + + DateTime initDateTime = DateTimeFromBytes(initDate); + DateTime writeDateTime = DateTimeFromBytes(writeDate); + + Console.WriteLine($"Parsed Init Date: {initDateTime}"); + Console.WriteLine($"Parsed Write Date: {writeDateTime}"); + + virtualAmiiboFile.FirstWriteDate = initDateTime; + virtualAmiiboFile.LastWriteDate = writeDateTime; + virtualAmiiboFile.WriteCounter = BitConverter.ToUInt16(writeCounter, 0); + + // Parse application areas + //List applicationAreasList = ParseAmiiboData(applicationAreas); + List applicationAreasList = new List(); + virtualAmiiboFile.ApplicationAreas = applicationAreasList; + + // Save the virtual Amiibo file + VirtualAmiibo.SaveAmiiboFile(virtualAmiiboFile); + + return virtualAmiiboFile; + } + + private static string GetKeyRetailBinPath() + { + return Path.Combine(AppDataManager.KeysDirPath, "key_retail.bin"); + } + + public static bool IsPageEncrypted(int page) + { + return (page >= 6 && page <= 9) || (page >= 43 && page <= 84); + } + + public static DateTime DateTimeFromBytes(byte[] date) + { + if (date == null || date.Length != 2) + { + Console.WriteLine("Invalid date bytes."); + return DateTime.MinValue; + } + + ushort value = BitConverter.ToUInt16(date, 0); + + int day = value & 0x1F; + int month = (value >> 5) & 0x0F; + int year = (value >> 9) & 0x7F; + + try + { + return new DateTime(2000 + year, month, day); + } + catch (ArgumentOutOfRangeException) + { + Console.WriteLine("Invalid date values extracted."); + return DateTime.MinValue; + } + } + + public static List ParseAmiiboData(byte[] decryptedData) + { + return JsonSerializer.Deserialize>(decryptedData); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs new file mode 100644 index 000000000..b2b309828 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboDecrypter.cs @@ -0,0 +1,113 @@ +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.HLE.HOS.Services.Mii.Types; +using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager; +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Bin +{ + public class AmiiboDecrypter + { + public readonly byte[] _hmacKey; // HMAC key + public readonly byte[] _aesKey; // AES key + + public AmiiboDecrypter(string keyRetailBinPath) + { + var keys = AmiiboMasterKey.FromCombinedBin(File.ReadAllBytes(keyRetailBinPath)); + _hmacKey = keys.DataKey.HmacKey; + _aesKey = keys.DataKey.XorPad; + } + + public byte[] DecryptAmiiboData(byte[] encryptedData, byte[] counter) + { + // Ensure the counter length matches the block size + if (counter.Length != 16) + { + throw new ArgumentException("Counter must be 16 bytes long for AES block size."); + } + + byte[] decryptedData = new byte[encryptedData.Length]; + + using (Aes aesAlg = Aes.Create()) + { + aesAlg.Key = _aesKey; + aesAlg.Mode = CipherMode.ECB; // Use ECB mode to handle the counter encryption + aesAlg.Padding = PaddingMode.None; + + using (var encryptor = aesAlg.CreateEncryptor()) + { + int blockSize = 16; + byte[] encryptedCounter = new byte[blockSize]; + byte[] currentCounter = (byte[])counter.Clone(); + + for (int i = 0; i < encryptedData.Length; i += blockSize) + { + // Encrypt the current counter block + encryptor.TransformBlock(currentCounter, 0, blockSize, encryptedCounter, 0); + + // XOR the encrypted counter with the ciphertext to get the decrypted data + for (int j = 0; j < blockSize && i + j < encryptedData.Length; j++) + { + decryptedData[i + j] = (byte)(encryptedData[i + j] ^ encryptedCounter[j]); + } + + // Increment the counter for the next block + IncrementCounter(currentCounter); + } + } + } + + return decryptedData; + } + + public byte[] CalculateHMAC(byte[] data) + { + using (var hmac = new HMACSHA256(_hmacKey)) + { + return hmac.ComputeHash(data); + } + } + + public void IncrementCounter(byte[] counter) + { + for (int i = counter.Length - 1; i >= 0; i--) + { + if (++counter[i] != 0) + break; // Stop if no overflow + } + } + + public DateTime ParseDate(byte[] data, int offset) + { + ushort year = BitConverter.ToUInt16(data, offset); + byte month = data[offset + 2]; + byte day = data[offset + 3]; + byte hour = data[offset + 4]; + byte minute = data[offset + 5]; + byte second = data[offset + 6]; + + return new DateTime(year, month, day, hour, minute, second); + } + + public List ParseApplicationAreas(byte[] data, int startOffset, int areaSize) + { + var areas = new List(); + for (int i = 0; i < 8; i++) // Assuming 8 areas + { + int offset = startOffset + (i * areaSize); + string applicationId = BitConverter.ToString(data[offset..(offset + 4)]).Replace("-", ""); + byte[] areaData = data[(offset + 4)..(offset + areaSize)]; + areas.Add(new VirtualAmiiboApplicationArea + { + ApplicationAreaId = uint.Parse(applicationId), + ApplicationArea = areaData + }); + } + + return areas; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboMasterKey.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboMasterKey.cs new file mode 100644 index 000000000..9ec2d0b80 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboMasterKey.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Bin +{ + public class AmiiboMasterKey + { + private const int DataLength = 80; + private const int CombinedLength = 160; + public byte[] HmacKey { get; private set; } // 16 bytes + public byte[] TypeString { get; private set; } // 14 bytes + public byte Rfu { get; private set; } // 1 byte reserved + public byte MagicSize { get; private set; } // 1 byte + public byte[] MagicBytes { get; private set; } // 16 bytes + public byte[] XorPad { get; private set; } // 32 bytes + + private AmiiboMasterKey(byte[] data) + { + if (data.Length != DataLength) + throw new ArgumentException($"Data is {data.Length} bytes (should be {DataLength})."); + + + // Unpack the data + HmacKey = data[..16]; + TypeString = data[16..30]; + Rfu = data[30]; + MagicSize = data[31]; + MagicBytes = data[32..48]; + XorPad = data[48..]; + } + + public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromSeparateBin(byte[] dataBin, byte[] tagBin) + { + var dataKey = new AmiiboMasterKey(dataBin); + var tagKey = new AmiiboMasterKey(tagBin); + return (dataKey, tagKey); + } + + public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromSeparateHex(string dataHex, string tagHex) + { + return FromSeparateBin(HexToBytes(dataHex), HexToBytes(tagHex)); + } + + public static (AmiiboMasterKey DataKey, AmiiboMasterKey TagKey) FromCombinedBin(byte[] combinedBin) + { + if (combinedBin.Length != CombinedLength) + throw new ArgumentException($"Data is {combinedBin.Length} bytes (should be {CombinedLength})."); + + byte[] dataBin = combinedBin[..DataLength]; + byte[] tagBin = combinedBin[DataLength..]; + return FromSeparateBin(dataBin, tagBin); + } + + private static byte[] HexToBytes(string hex) + { + int length = hex.Length / 2; + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) + { + bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); + } + return bytes; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs index e1db98e5f..9450e1db5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/Types/VirtualAmiiboFile.cs @@ -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 ApplicationAreas { get; set; } } - struct VirtualAmiiboApplicationArea + public struct VirtualAmiiboApplicationArea { public uint ApplicationAreaId { get; set; } public byte[] ApplicationArea { get; set; } diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs index 0c685471c..579c9157e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs @@ -204,7 +204,7 @@ 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); diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index ee0d03171..f67b83c7a 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -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", diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 3672f8c71..83443c6d8 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -28,12 +28,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.Bin; 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; @@ -2059,6 +2061,32 @@ namespace Ryujinx.Ava.UI.ViewModels } } } + public async Task OpenBinFile() + { + if (!IsAmiiboRequested) + return; + + if (AppHost.Device.System.SearchingForAmiibo(out int deviceId)) + { + var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle], + AllowMultiple = false, + FileTypeFilter = new List + { + new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats]) + { + Patterns = new[] { "*.bin" }, + } + } + }); + if (result.Count > 0) + { + AppHost.Device.System.ScanAmiiboFromBin(result[0].Path.LocalPath); + } + } + } + public void ToggleFullscreen() { diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index c5e794da2..153cfd379 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -241,6 +241,13 @@ Icon="{ext:Icon mdi-cube-scan}" InputGesture="Ctrl + A" IsEnabled="{Binding IsAmiiboRequested}" /> + 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)