diff --git a/Directory.Packages.props b/Directory.Packages.props index fd61602a8..fba3792db 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,7 +41,7 @@ - + @@ -56,7 +56,6 @@ - - \ No newline at end of file + diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index 9ecf9826c..3d23d4286 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -700,6 +700,56 @@ "zh_TW": "掃描 Amiibo" } }, + { + "ID": "MenuBarActionsScanSkylander", + "Translations": { + "ar_SA": "‫فحص Skylander", + "de_DE": "Skylander scannen", + "el_GR": "Σάρωση Skylander", + "en_US": "Scan A Skylander", + "es_ES": "Escanear Skylander", + "fr_FR": "Scanner un Skylander", + "he_IL": "סרוק אמיבו", + "it_IT": "Scansiona un Skylander", + "ja_JP": "Skylander をスキャン", + "ko_KR": "Skylander 스캔", + "no_NO": "Skann en Skylander", + "pl_PL": "Skanuj Skylander", + "pt_BR": "Escanear um Skylander", + "ru_RU": "Сканировать Skylander", + "sv_SE": "Skanna en Skylander", + "th_TH": "สแกนหา Skylander", + "tr_TR": "Bir Skylander Tara", + "uk_UA": "Сканувати Skylander", + "zh_CN": "扫描 Skylander", + "zh_TW": "掃描 Skylander" + } + }, + { + "ID": "MenuBarActionsRemoveSkylander", + "Translations": { + "ar_SA": "إزالة Skylander", + "de_DE": "Skylander entfernen", + "el_GR": "Αφαίρεση Skylander", + "en_US": "Remove Skylander", + "es_ES": "Eliminar Skylander", + "fr_FR": "Supprimer un Skylander", + "he_IL": "הסר Skylander", + "it_IT": "Rimuovi Skylander", + "ja_JP": "Skylander を削除", + "ko_KR": "Skylander 제거", + "no_NO": "Fjern Skylander", + "pl_PL": "Usuń Skylander", + "pt_BR": "Remover um Skylander", + "ru_RU": "Удалить Skylander", + "sv_SE": "Ta bort Skylander", + "th_TH": "ลบ Skylander", + "tr_TR": "Skylander'ı Kaldır", + "uk_UA": "Видалити Skylander", + "zh_CN": "移除 Skylander", + "zh_TW": "移除 Skylander" + } + }, { "ID": "MenuBarActionsScanAmiiboBin", "Translations": { diff --git a/docs/compatibility.csv b/docs/compatibility.csv index e476f9253..fa804c909 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -2050,7 +2050,9 @@ 010003C00B868000,"Ninjin: Clash of Carrots",online-broken,playable,2024-07-10 05:12:26 0100746010E4C000,"NinNinDays",,playable,2022-11-20 15:17:29 0100C9A00ECE6000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07 +010057D00ECE4000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07 0100e0601c632000,"Nintendo 64™ – Nintendo Switch Online: MATURE 17+",,ingame,2025-02-03 22:27:00 +010037A0170D2000,"NINTENDO 64™ – Nintendo Switch Online 18+",,ingame,2025-02-03 22:27:00 0100D870045B6000,"Nintendo Entertainment System™ - Nintendo Switch Online",online,playable,2022-07-01 15:45:06 0100C4B0034B2000,"Nintendo Labo Toy-Con 01 Variety Kit",gpu,ingame,2022-08-07 12:56:07 01001E9003502000,"Nintendo Labo Toy-Con 03 Vehicle Kit",services;crash,menus,2022-08-03 17:20:11 @@ -2638,6 +2640,7 @@ 0100B16009C10000,"SINNER: Sacrifice for Redemption",nvdec;UE4;vulkan-backend-bug,playable,2022-08-12 20:37:33 0100E9201410E000,"Sir Lovelot",,playable,2021-04-05 16:21:46 0100134011E32000,"Skate City",,playable,2022-11-04 11:37:39 +0100a8501b66e000,"Skateboard Drifting with Maxwell Cat: The Game Simulator",,playable,2026-02-17 19:05:00 0100B2F008BD8000,"Skee-Ball",,playable,2020-11-16 04:44:07 01001A900F862000,"Skelattack",,playable,2021-06-09 15:26:26 01008E700F952000,"Skelittle: A Giant Party!",,playable,2021-06-09 19:08:34 @@ -3307,6 +3310,7 @@ 0100AFA011068000,"Voxel Pirates",,playable,2022-09-28 22:55:02 0100BFB00D1F4000,"Voxel Sword",,playable,2022-08-30 14:57:27 01004E90028A2000,"Vroom in the night sky",Needs Update;vulkan-backend-bug,playable,2023-02-20 02:32:29 +0100BFC01D976000,"Virtual Boy – Nintendo Classics",services,nothing,2026-02-17 11:26:59 0100C7C00AE6C000,"VSR: Void Space Racing",,playable,2021-01-27 14:08:59 0100B130119D0000,"Waifu Uncovered",crash,ingame,2023-02-27 01:17:46 0100E29010A4A000,"Wanba Warriors",,playable,2020-10-04 17:56:22 diff --git a/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs b/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs index a97e7b409..24a318be4 100644 --- a/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs +++ b/src/Ryujinx.BuildValidationTasks/LocalesValidationTask.cs @@ -107,12 +107,12 @@ namespace Ryujinx.BuildValidationTasks { locale.Translations[langCode] = string.Empty; Console.WriteLine( - $"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it..."); + $"Language '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it..."); } else { Console.WriteLine( - $"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!"); + $"Language '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!"); } } diff --git a/src/Ryujinx.Common/Ryujinx.Common.csproj b/src/Ryujinx.Common/Ryujinx.Common.csproj index e31d2f3bc..60a75a7b2 100644 --- a/src/Ryujinx.Common/Ryujinx.Common.csproj +++ b/src/Ryujinx.Common/Ryujinx.Common.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs index f77a2858f..c232cfd01 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -184,6 +184,7 @@ namespace Ryujinx.Common "01001b300b9be000", // Diablo III: Eternal Collection "010027400cdc6000", // Divinity Original 2 - Definitive Edition "01008c8012920000", // Dying Light Platinum Edition + "0100d11013e6a000", // Eschatos "01001cc01b2d4000", // Goat Simulator 3 "01003620068ea000", // Hand of Fate 2 "0100f7e00c70e000", // Hogwarts Legacy @@ -193,9 +194,15 @@ namespace Ryujinx.Common "0100d71004694000", // Minecraft "01007430037f6000", // Monopoly "0100853015e86000", // No Man's Sky + "0100f85014ed0000", // No More Heroes + "0100463014ed4000", // No More Heroes 2 + "0100e570094e8000", // Owlboy "01007bb017812000", // Portal "0100abd01785c000", // Portal 2 + "01009f100bc52000", // Psikyo Collection 1 + "01009d400c4a8000", // Psikyo Collection 2 "01008e200c5c2000", // Muse Dash + "01005ff002e2a000", // Rayman Legends "01007820196a6000", // Red Dead Redemption "0100e8300a67a000", // Risk "01002f7013224000", // Rune Factory 5 diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index 83aaa1f4d..38d504bbf 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -20,6 +20,7 @@ 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.Nfc.Mifare.MifareManager; using Ryujinx.HLE.HOS.Services.Nv; using Ryujinx.HLE.HOS.Services.Nv.NvDrvServices.NvHostCtrl; using Ryujinx.HLE.HOS.Services.Pcv.Bpc; @@ -66,6 +67,8 @@ namespace Ryujinx.HLE.HOS internal List NfpDevices { get; private set; } + internal List NfcDevices { get; private set; } + internal SmRegistry SmRegistry { get; private set; } internal ServerBase SmServer { get; private set; } @@ -132,6 +135,7 @@ namespace Ryujinx.HLE.HOS PerformanceState = new PerformanceState(); NfpDevices = []; + NfcDevices = []; // Note: This is not really correct, but with HLE of services, the only memory // region used that is used is Application, so we can use the other ones for anything. @@ -372,6 +376,15 @@ namespace Ryujinx.HLE.HOS } } + public void ScanSkylander(int nfcDeviceId, byte[] data) + { + if (NfcDevices[nfcDeviceId].State == NfcDeviceState.SearchingForTag) + { + NfcDevices[nfcDeviceId].State = NfcDeviceState.TagFound; + NfcDevices[nfcDeviceId].Data = data; + } + } + public bool SearchingForAmiibo(out int nfpDeviceId) { nfpDeviceId = default; @@ -389,6 +402,53 @@ namespace Ryujinx.HLE.HOS return false; } + public bool SearchingForSkylander(out int nfcDeviceId) + { + nfcDeviceId = default; + + for (int i = 0; i < NfcDevices.Count; i++) + { + if (NfcDevices[i].State == NfcDeviceState.SearchingForTag) + { + nfcDeviceId = i; + + return true; + } + } + + return false; + } + + public bool HasSkylander(out int nfcDeviceId) + { + nfcDeviceId = default; + + for (int i = 0; i < NfcDevices.Count; i++) + { + if (NfcDevices[i].State == NfcDeviceState.TagFound) + { + nfcDeviceId = i; + + return true; + } + } + + return false; + } + + public void RemoveSkylander() + { + for (int i = 0; i < NfcDevices.Count; i++) + { + if (NfcDevices[i].State == NfcDeviceState.TagFound) + { + NfcDevices[i].State = NfcDeviceState.Initialized; + NfcDevices[i].SignalDeactivate(); + Thread.Sleep(100); // NOTE: Simulate skylander scanning delay. + } + } + } + public void SignalDisplayResolutionChange() { DisplayResolutionChangeEvent.ReadableEvent.Signal(); diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/IUserManager.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/IUserManager.cs index 295c7e71a..53b6549c5 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/IUserManager.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/IUserManager.cs @@ -1,8 +1,19 @@ +using Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager; + namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare { [Service("nfc:mf:u")] class IUserManager : IpcService { public IUserManager(ServiceCtx context) { } + + [CommandCmif(0)] + // CreateUserInterface() -> object + public ResultCode CreateUserInterface(ServiceCtx context) + { + MakeObject(context, new IMifare()); + + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/IMifare.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/IMifare.cs new file mode 100644 index 000000000..43e28b5cf --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/IMifare.cs @@ -0,0 +1,477 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.HLE.HOS.Services.Hid; +using Ryujinx.HLE.HOS.Services.Hid.HidServer; +using Ryujinx.Horizon.Common; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + class IMifare : IpcService + { + private State _state; + + private KEvent _availabilityChangeEvent; + + private CancellationTokenSource _cancelTokenSource; + + public IMifare() + { + _state = State.NonInitialized; + } + + [CommandCmif(0)] + public ResultCode Initialize(ServiceCtx context) + { + _state = State.Initialized; + + NfcDevice devicePlayer1 = new() + { + NpadIdType = NpadIdType.Player1, + Handle = HidUtils.GetIndexFromNpadIdType(NpadIdType.Player1), + State = NfcDeviceState.Initialized, + }; + + context.Device.System.NfcDevices.Add(devicePlayer1); + + return ResultCode.Success; + } + + [CommandCmif(1)] + public ResultCode Finalize(ServiceCtx context) + { + if (_state == State.Initialized) + { + _cancelTokenSource?.Cancel(); + + // NOTE: All events are destroyed here. + context.Device.System.NfcDevices.Clear(); + + _state = State.NonInitialized; + } + + return ResultCode.Success; + } + + [CommandCmif(2)] + public ResultCode GetListDevices(ServiceCtx context) + { + if (context.Request.RecvListBuff.Count == 0) + { + return ResultCode.WrongArgument; + } + + ulong outputPosition = context.Request.RecvListBuff[0].Position; + ulong outputSize = context.Request.RecvListBuff[0].Size; + + if (context.Device.System.NfcDevices.Count == 0) + { + return ResultCode.DeviceNotFound; + } + + MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + context.Memory.Write(outputPosition + ((uint)i * sizeof(long)), (uint)context.Device.System.NfcDevices[i].Handle); + } + + context.ResponseData.Write(context.Device.System.NfcDevices.Count); + + return ResultCode.Success; + } + + [CommandCmif(3)] + public ResultCode StartDetection(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + context.Device.System.NfcDevices[i].State = NfcDeviceState.SearchingForTag; + + break; + } + } + + _cancelTokenSource = new CancellationTokenSource(); + + Task.Run(() => + { + while (true) + { + if (_cancelTokenSource.Token.IsCancellationRequested) + { + break; + } + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagFound) + { + context.Device.System.NfcDevices[i].SignalActivate(); + Thread.Sleep(125); // NOTE: Simulate skylander scanning delay. + + break; + } + } + } + }, _cancelTokenSource.Token); + + return ResultCode.Success; + } + + [CommandCmif(4)] + public ResultCode StopDetection(ServiceCtx context) + { + _cancelTokenSource?.Cancel(); + + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + context.Device.System.NfcDevices[i].State = NfcDeviceState.Initialized; + Array.Clear(context.Device.System.NfcDevices[i].Data); + context.Device.System.NfcDevices[i].SignalDeactivate(); + + break; + } + } + + return ResultCode.Success; + } + + [CommandCmif(5)] + public ResultCode ReadMifare(ServiceCtx context) + { + if (context.Request.ReceiveBuff.Count == 0 || context.Request.SendBuff.Count == 0) + { + return ResultCode.WrongArgument; + } + + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + if (context.Device.System.NfcDevices.Count == 0) + { + return ResultCode.DeviceNotFound; + } + + ulong outputPosition = context.Request.ReceiveBuff[0].Position; + ulong outputSize = context.Request.ReceiveBuff[0].Size; + + MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize); + + ulong inputPosition = context.Request.SendBuff[0].Position; + ulong inputSize = context.Request.SendBuff[0].Size; + + byte[] readBlockParameter = new byte[inputSize]; + + context.Memory.Read(inputPosition, readBlockParameter); + + var span = MemoryMarshal.Cast(readBlockParameter); + var list = new List(span.Length); + + foreach (var item in span) + list.Add(item); + + Thread.Sleep(125 * list.Count); // NOTE: Simulate skylander scanning delay. + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved) + { + return ResultCode.TagNotFound; + } + else + { + for (int p = 0; p < list.Count; p++) + { + NfcMifareReadBlockData blockData = new() + { + SectorNumber = list[p].SectorNumber, + Reserved = new Array7(), + }; + byte[] data = new byte[16]; + + switch (list[p].SectorKey.MifareCommand) + { + case NfcMifareCommand.NfcMifareCommand_Read: + case NfcMifareCommand.NfcMifareCommand_AuthA: + if (IsCurrentBlockKeyBlock(list[p].SectorNumber)) + { + Array.Copy(context.Device.System.NfcDevices[i].Data, (16 * list[p].SectorNumber) + 6, data, 6, 4); + } + else + { + Array.Copy(context.Device.System.NfcDevices[i].Data, 16 * list[p].SectorNumber, data, 0, 16); + } + data.CopyTo(blockData.Data.AsSpan()); + context.Memory.Write(outputPosition + ((uint)(p * Unsafe.SizeOf())), blockData); + break; + } + } + } + } + } + + return ResultCode.Success; + } + + [CommandCmif(6)] + public ResultCode WriteMifare(ServiceCtx context) + { + if (context.Request.SendBuff.Count == 0) + { + return ResultCode.WrongArgument; + } + + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + if (context.Device.System.NfcDevices.Count == 0) + { + return ResultCode.DeviceNotFound; + } + + ulong inputPosition = context.Request.SendBuff[0].Position; + ulong inputSize = context.Request.SendBuff[0].Size; + + byte[] writeBlockParameter = new byte[inputSize]; + + context.Memory.Read(inputPosition, writeBlockParameter); + + var span = MemoryMarshal.Cast(writeBlockParameter); + var list = new List(span.Length); + + foreach (var item in span) + list.Add(item); + + Thread.Sleep(125 * list.Count); // NOTE: Simulate skylander scanning delay. + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved) + { + return ResultCode.TagNotFound; + } + else + { + for (int p = 0; p < list.Count; p++) + { + switch (list[p].SectorKey.MifareCommand) + { + case NfcMifareCommand.NfcMifareCommand_Write: + case NfcMifareCommand.NfcMifareCommand_AuthA: + list[p].Data.AsSpan().CopyTo(context.Device.System.NfcDevices[i].Data.AsSpan(list[p].SectorNumber * 16, 16)); + break; + } + } + } + } + } + + return ResultCode.Success; + } + + [CommandCmif(7)] + public ResultCode GetTagInfo(ServiceCtx context) + { + ResultCode resultCode = ResultCode.Success; + + if (context.Request.RecvListBuff.Count == 0) + { + return ResultCode.WrongArgument; + } + + ulong outputPosition = context.Request.RecvListBuff[0].Position; + + context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize((uint)Marshal.SizeOf()); + + MemoryHelper.FillWithZeros(context.Memory, outputPosition, Marshal.SizeOf()); + + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + if (context.Device.System.NfcDevices.Count == 0) + { + return ResultCode.DeviceNotFound; + } + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if (context.Device.System.NfcDevices[i].Handle == (PlayerIndex)deviceHandle) + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagRemoved) + { + resultCode = ResultCode.TagNotFound; + } + else + { + if (context.Device.System.NfcDevices[i].State == NfcDeviceState.TagMounted || context.Device.System.NfcDevices[i].State == NfcDeviceState.TagFound) + { + TagInfo tagInfo = new() + { + UuidLength = 4, + Reserved1 = new Array21(), + Protocol = (uint)NfcProtocol.NfcProtocol_TypeA, // Type A Protocol + TagType = (uint)NfcTagType.NfcTagType_Mifare, // Mifare Type + Reserved2 = new Array6(), + }; + + byte[] uuid = new byte[4]; + + Array.Copy(context.Device.System.NfcDevices[i].Data, 0, uuid, 0, 4); + + uuid.CopyTo(tagInfo.Uuid.AsSpan()); + + context.Memory.Write(outputPosition, tagInfo); + + resultCode = ResultCode.Success; + } + else + { + resultCode = ResultCode.WrongDeviceState; + } + } + + break; + } + } + + return resultCode; + } + + [CommandCmif(8)] + public ResultCode AttachActivateEvent(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle) + { + context.Device.System.NfcDevices[i].ActivateEvent = new KEvent(context.Device.System.KernelContext); + + if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfcDevices[i].ActivateEvent.ReadableEvent, out int activateEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(activateEventHandle); + + return ResultCode.Success; + } + } + + return ResultCode.DeviceNotFound; + } + + [CommandCmif(9)] + public ResultCode AttachDeactivateEvent(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle) + { + context.Device.System.NfcDevices[i].DeactivateEvent = new KEvent(context.Device.System.KernelContext); + + if (context.Process.HandleTable.GenerateHandle(context.Device.System.NfcDevices[i].DeactivateEvent.ReadableEvent, out int deactivateEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(deactivateEventHandle); + + return ResultCode.Success; + } + } + + return ResultCode.DeviceNotFound; + } + + [CommandCmif(10)] + public ResultCode GetState(ServiceCtx context) + { + context.ResponseData.Write((int)_state); + + return ResultCode.Success; + } + + [CommandCmif(11)] + public ResultCode GetDeviceState(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle) + { + if (context.Device.System.NfcDevices[i].State > NfcDeviceState.Finalized) + { + throw new InvalidOperationException($"{nameof(context.Device.System.NfcDevices)} contains an invalid state for device {i}: {context.Device.System.NfcDevices[i].State}"); + } + context.ResponseData.Write((uint)context.Device.System.NfcDevices[i].State); + + return ResultCode.Success; + } + } + + context.ResponseData.Write((uint)NfcDeviceState.Unavailable); + + return ResultCode.DeviceNotFound; + } + + [CommandCmif(12)] + public ResultCode GetNpadId(ServiceCtx context) + { + uint deviceHandle = (uint)context.RequestData.ReadUInt64(); + + for (int i = 0; i < context.Device.System.NfcDevices.Count; i++) + { + if ((uint)context.Device.System.NfcDevices[i].Handle == deviceHandle) + { + context.ResponseData.Write((uint)HidUtils.GetNpadIdTypeFromIndex(context.Device.System.NfcDevices[i].Handle)); + + return ResultCode.Success; + } + } + + return ResultCode.DeviceNotFound; + } + + [CommandCmif(13)] + public ResultCode AttachAvailabilityChangeEvent(ServiceCtx context) + { + _availabilityChangeEvent = new KEvent(context.Device.System.KernelContext); + + if (context.Process.HandleTable.GenerateHandle(_availabilityChangeEvent.ReadableEvent, out int availabilityChangeEventHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(availabilityChangeEventHandle); + + return ResultCode.Success; + } + + private bool IsCurrentBlockKeyBlock(byte block) + { + return ((block + 1) % 4) == 0; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcDevice.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcDevice.cs new file mode 100644 index 000000000..5d2b97fe9 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcDevice.cs @@ -0,0 +1,21 @@ +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.HLE.HOS.Services.Hid; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + class NfcDevice + { + public KEvent ActivateEvent; + public KEvent DeactivateEvent; + + public void SignalActivate() => ActivateEvent.ReadableEvent.Signal(); + public void SignalDeactivate() => DeactivateEvent.ReadableEvent.Signal(); + + public NfcDeviceState State = NfcDeviceState.Unavailable; + + public PlayerIndex Handle; + public NpadIdType NpadIdType; + + public byte[] Data; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareCommand.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareCommand.cs new file mode 100644 index 000000000..0ab18c183 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareCommand.cs @@ -0,0 +1,14 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum NfcMifareCommand : byte + { + NfcMifareCommand_Read = 0x30, + NfcMifareCommand_AuthA = 0x60, + NfcMifareCommand_AuthB = 0x61, + NfcMifareCommand_Write = 0xA0, + NfcMifareCommand_Transfer = 0xB0, + NfcMifareCommand_Decrement = 0xC0, + NfcMifareCommand_Increment = 0xC1, + NfcMifareCommand_Store = 0xC2, + } +} \ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockData.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockData.cs new file mode 100644 index 000000000..bc5f1dcf3 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockData.cs @@ -0,0 +1,13 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18)] + struct NfcMifareReadBlockData + { + public Array16 Data; + public byte SectorNumber; + public Array7 Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockParameter.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockParameter.cs new file mode 100644 index 000000000..df4ed6fce --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareReadBlockParameter.cs @@ -0,0 +1,13 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18)] + struct NfcMifareReadBlockParameter + { + public byte SectorNumber; + public Array7 Reserved; + public NfcSectorKey SectorKey; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareWriteBlockParameter.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareWriteBlockParameter.cs new file mode 100644 index 000000000..fcdbfab2d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcMifareWriteBlockParameter.cs @@ -0,0 +1,14 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x28)] + struct NfcMifareWriteBlockParameter + { + public Array16 Data; + public byte SectorNumber; + public Array7 Reserved; + public NfcSectorKey SectorKey; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcProtocol.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcProtocol.cs new file mode 100644 index 000000000..d486ddade --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcProtocol.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum NfcProtocol : byte + { + NfcProtocol_None = 0b_0000_0000, + NfcProtocol_TypeA = 0b_0000_0001, ///< ISO14443A + NfcProtocol_TypeB = 0b_0000_0010, ///< ISO14443B + NfcProtocol_TypeF = 0b_0000_0100, ///< Sony FeliCa + NfcProtocol_All = 0xFF, + } +} \ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcSectorKey.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcSectorKey.cs new file mode 100644 index 000000000..bd7d51813 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcSectorKey.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + struct NfcSectorKey + { + public NfcMifareCommand MifareCommand; + public byte Unknown; + public Array6 Reserved1; + public Array6 SectorKey; + public Array2 Reserved2; + + } +} \ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcTagType.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcTagType.cs new file mode 100644 index 000000000..b21c4c9d0 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfcTagType.cs @@ -0,0 +1,15 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum NfcTagType : byte + { + NfcTagType_None = 0b_0000_0000, + NfcTagType_Type1 = 0b_0000_0001, ///< ISO14443A RW. Topaz + NfcTagType_Type2 = 0b_0000_0010, ///< ISO14443A RW. Ultralight, NTAGX, ST25TN + NfcTagType_Type3 = 0b_0000_0100, ///< ISO14443A RW/RO. Sony FeliCa + NfcTagType_Type4A = 0b_0000_1000, ///< ISO14443A RW/RO. DESFire + NfcTagType_Type4B = 0b_0001_0000, ///< ISO14443B RW/RO. DESFire + NfcTagType_Type5 = 0b_0010_0000, ///< ISO15693 RW/RO. SLI, SLIX, ST25TV + NfcTagType_Mifare = 0b_0100_0000, ///< Mifare clasic. Skylanders + NfcTagType_All = 0xFF, + } +} \ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfpDeviceState.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfpDeviceState.cs new file mode 100644 index 000000000..9e51b8d4d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/NfpDeviceState.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum NfcDeviceState : byte + { + Initialized = 0, + SearchingForTag = 1, + TagFound = 2, + TagRemoved = 3, + TagMounted = 4, + Unavailable = 5, + Finalized = 6, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/State.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/State.cs new file mode 100644 index 000000000..a9ee720e9 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/State.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + enum State + { + NonInitialized, + Initialized, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/TagInfo.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/TagInfo.cs new file mode 100644 index 000000000..5db4612c9 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/MifareManager/Types/TagInfo.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager +{ + [StructLayout(LayoutKind.Sequential, Size = 0x58)] + struct TagInfo + { + public Array10 Uuid; + public byte UuidLength; + public Array21 Reserved1; + public uint Protocol; + public uint TagType; + public Array6 Reserved2; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/ResultCode.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/ResultCode.cs new file mode 100644 index 000000000..3148e02e4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Mifare/ResultCode.cs @@ -0,0 +1,17 @@ +namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare +{ + public enum ResultCode + { + ModuleId = 161, + ErrorCodeShift = 9, + + Success = 0, + + DeviceNotFound = (64 << ErrorCodeShift) | ModuleId, // 0x80A1 + WrongArgument = (65 << ErrorCodeShift) | ModuleId, // 0x82A1 + WrongDeviceState = (73 << ErrorCodeShift) | ModuleId, // 0x92A1 + NfcDisabled = (80 << ErrorCodeShift) | ModuleId, // 0xA0A1 + TagNotFound = (97 << ErrorCodeShift) | ModuleId, // 0xC2A1 + MifareAccessError = (288 << ErrorCodeShift) | ModuleId, // 0x240a1 + } +} diff --git a/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs b/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs index f5b7fc0f1..8e7a44005 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs @@ -20,5 +20,7 @@ namespace Ryujinx.HLE.HOS.SystemState SimplifiedChinese, TraditionalChinese, BrazilianPortuguese, + Polish, + Thai, } } diff --git a/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs b/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs index 91277232c..74378b153 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs @@ -23,7 +23,9 @@ namespace Ryujinx.HLE.HOS.SystemState "es-419", "zh-Hans", "zh-Hant", - "pt-BR" + "pt-BR", + "pl", + "th" ]; internal long DesiredKeyboardLayout { get; private set; } diff --git a/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs b/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs index e3bfb9165..e651410ce 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs @@ -18,5 +18,7 @@ namespace Ryujinx.HLE.HOS.SystemState TraditionalChinese, SimplifiedChinese, BrazilianPortuguese, + Polish, + Thai, } } diff --git a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs index 7640967c6..e6a65feef 100644 --- a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs +++ b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs @@ -1,12 +1,19 @@ using Ryujinx.Common.Memory; using System; +using System.Buffers.Binary; +using System.IO; +using System.IO.Compression; +using System.Runtime.InteropServices; using System.Text; namespace Ryujinx.Horizon.Sdk.Ns { public struct ApplicationControlProperty { - public Array16 Title; + /// + /// Use to access titles instead of accessing them directly. + /// + public Array16 TitleBlock; public Array37 Isbn; public StartupUserAccountValue StartupUserAccount; public UserAccountSwitchLockValue UserAccountSwitchLock; @@ -58,7 +65,10 @@ namespace Ryujinx.Horizon.Sdk.Ns public RepairFlagValue RepairFlag; public byte ProgramIndex; public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag; - public Array4 Reserved3214; + public byte ApplicationErrorCodePrefix; + public TitleCompressionValue TitleCompression; + public byte AcdIndex; + public byte ApparentPlatform; public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration; public ApplicationJitConfiguration JitConfiguration; public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors; @@ -74,6 +84,47 @@ namespace Ryujinx.Horizon.Sdk.Ns public readonly string DisplayVersionString => Encoding.UTF8.GetString(DisplayVersion.AsSpan()).TrimEnd('\0'); public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0'); public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0'); + + private const int TitleCount = 32; + private const int TitleEntrySize = 0x300; + + + /// + /// Returns the resolved title entries. When is + /// , the raw bytes are + /// decompressed (raw deflate) from 0x3000 into 0x6000 bytes yielding up to 32 entries. + /// Otherwise the 16 uncompressed entries from are returned directly. + /// + public readonly ApplicationTitle[] Title + { + get + { + var titles = new ApplicationTitle[TitleCount]; + + if (TitleCompression != TitleCompressionValue.Enable) + { + TitleBlock.AsSpan().CopyTo(titles); + + return titles; + } + + ReadOnlySpan titleBytes = MemoryMarshal.AsBytes(TitleBlock.AsSpan()); + ushort compressedBlobSize = BinaryPrimitives.ReadUInt16LittleEndian(titleBytes); + ReadOnlySpan compressedBlob = titleBytes.Slice(2, compressedBlobSize); + + byte[] decompressed = new byte[TitleCount * TitleEntrySize]; + + using (var compressedStream = new MemoryStream(compressedBlob.ToArray())) + using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress)) + { + deflateStream.ReadExactly(decompressed, 0, decompressed.Length); + } + + MemoryMarshal.Cast(decompressed).CopyTo(titles); + + return titles; + } + } public struct ApplicationTitle { @@ -130,6 +181,8 @@ namespace Ryujinx.Horizon.Sdk.Ns TraditionalChinese = 13, SimplifiedChinese = 14, BrazilianPortuguese = 15, + Polish = 16, + Thai = 17, } public enum Organization @@ -302,5 +355,11 @@ namespace Ryujinx.Horizon.Sdk.Ns Deny = 0, Allow = 1, } + + public enum TitleCompressionValue : byte + { + Disable = 0, + Enable = 1, + } } } diff --git a/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs b/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs index 48e49a7fa..4aa8692dd 100644 --- a/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs +++ b/src/Ryujinx/Input/AvaloniaKeyboardMappingHelper.cs @@ -141,7 +141,7 @@ namespace Ryujinx.Ava.Input AvaKey.OemComma, AvaKey.OemPeriod, AvaKey.OemQuestion, - AvaKey.OemBackslash, + AvaKey.OemPipe, // NOTE: invalid AvaKey.None diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index 5da152501..715460274 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -28,11 +28,6 @@ true partial - - - true - false -