Compare commits

..

9 Commits

Author SHA1 Message Date
Joshua de Reeper
b70e2e44cb NFC Mifare Manager (ryubing/ryujinx!270)
See merge request ryubing/ryujinx!270
2026-02-21 05:45:00 -06:00
sh0inx
012d1d6886 Fixed spelling in LocalesValidationTask.cs (ryubing/ryujinx!269)
See merge request ryubing/ryujinx!269
2026-02-21 04:37:02 -06:00
BowedCascade
d1205dc95d Fix backslash key not mappable in controller settings (ryubing/ryujinx!265)
See merge request ryubing/ryujinx!265
2026-02-18 18:13:15 -06:00
Awesomeangotti
6f95172bb6 Compatability Data Update (ryubing/ryujinx!264)
See merge request ryubing/ryujinx!264
2026-02-17 19:24:01 -06:00
Princess Piplup
8208d43d9e compatiblity/2026-02-17 (ryubing/ryujinx!263)
See merge request ryubing/ryujinx!263
2026-02-17 18:57:50 -06:00
shinyoyo
1260f93aaf Updated ‌Simplified Chinese‌ translation. (ryubing/ryujinx!260)
See merge request ryubing/ryujinx!260
2026-02-09 01:07:22 -06:00
LotP
1b3bf1473d Fix Dual Joy-Con driver and InputView (ryubing/ryujinx!259)
See merge request ryubing/ryujinx!259
2026-01-31 23:12:29 -06:00
LotP
081cdcab0c remap joy-cons (ryubing/ryujinx!258)
See merge request ryubing/ryujinx!258
2026-01-31 17:58:31 -06:00
Coxxs
922775664c audio: Fix crash due to invalid Splitter size (ryubing/ryujinx!257)
See merge request ryubing/ryujinx!257
2026-01-31 11:22:14 -06:00
29 changed files with 1064 additions and 77 deletions

View File

@@ -21,7 +21,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "启动 RenderDoc 帧捕获",
"zh_TW": ""
}
},
@@ -46,7 +46,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "结束 RenderDoc 帧捕获",
"zh_TW": ""
}
},
@@ -71,7 +71,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "丢弃 RenderDoc 帧捕获",
"zh_TW": ""
}
},
@@ -96,7 +96,7 @@
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。",
"zh_TW": ""
}
}

View File

@@ -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": {

View File

@@ -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
1 title_id game_name labels status last_updated
2050 010003C00B868000 Ninjin: Clash of Carrots online-broken playable 2024-07-10 05:12:26
2051 0100746010E4C000 NinNinDays playable 2022-11-20 15:17:29
2052 0100C9A00ECE6000 Nintendo 64™ – Nintendo Switch Online gpu;vulkan ingame 2024-04-23 20:21:07
2053 010057D00ECE4000 Nintendo 64™ – Nintendo Switch Online gpu;vulkan ingame 2024-04-23 20:21:07
2054 0100e0601c632000 Nintendo 64™ – Nintendo Switch Online: MATURE 17+ ingame 2025-02-03 22:27:00
2055 010037A0170D2000 NINTENDO 64™ – Nintendo Switch Online 18+ ingame 2025-02-03 22:27:00
2056 0100D870045B6000 Nintendo Entertainment System™ - Nintendo Switch Online online playable 2022-07-01 15:45:06
2057 0100C4B0034B2000 Nintendo Labo Toy-Con 01 Variety Kit gpu ingame 2022-08-07 12:56:07
2058 01001E9003502000 Nintendo Labo Toy-Con 03 Vehicle Kit services;crash menus 2022-08-03 17:20:11
2640 0100B16009C10000 SINNER: Sacrifice for Redemption nvdec;UE4;vulkan-backend-bug playable 2022-08-12 20:37:33
2641 0100E9201410E000 Sir Lovelot playable 2021-04-05 16:21:46
2642 0100134011E32000 Skate City playable 2022-11-04 11:37:39
2643 0100a8501b66e000 Skateboard Drifting with Maxwell Cat: The Game Simulator playable 2026-02-17 19:05:00
2644 0100B2F008BD8000 Skee-Ball playable 2020-11-16 04:44:07
2645 01001A900F862000 Skelattack playable 2021-06-09 15:26:26
2646 01008E700F952000 Skelittle: A Giant Party! playable 2021-06-09 19:08:34
3310 0100AFA011068000 Voxel Pirates playable 2022-09-28 22:55:02
3311 0100BFB00D1F4000 Voxel Sword playable 2022-08-30 14:57:27
3312 01004E90028A2000 Vroom in the night sky Needs Update;vulkan-backend-bug playable 2023-02-20 02:32:29
3313 0100BFC01D976000 Virtual Boy – Nintendo Classics services nothing 2026-02-17 11:26:59
3314 0100C7C00AE6C000 VSR: Void Space Racing playable 2021-01-27 14:08:59
3315 0100B130119D0000 Waifu Uncovered crash ingame 2023-02-27 01:17:46
3316 0100E29010A4A000 Wanba Warriors playable 2020-10-04 17:56:22

View File

@@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Renderer.Common
public uint MixesSize;
public uint SinksSize;
public uint PerformanceBufferSize;
public uint Unknown24;
public uint SplitterSize;
public uint RenderInfoSize;
#pragma warning disable IDE0051, CS0169 // Remove unused field

View File

@@ -433,8 +433,12 @@ namespace Ryujinx.Audio.Renderer.Server
public ResultCode UpdateSplitter(SplitterContext context)
{
long initialInputConsumed = _inputReader.Consumed;
if (context.Update(ref _inputReader))
{
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.SplitterSize);
return ResultCode.Success;
}

View File

@@ -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}'!");
}
}

View File

@@ -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<NfpDevice> NfpDevices { get; private set; }
internal List<NfcDevice> 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();

View File

@@ -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<nn::nfc::mf::IUser>
public ResultCode CreateUserInterface(ServiceCtx context)
{
MakeObject(context, new IMifare());
return ResultCode.Success;
}
}
}

View File

@@ -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<byte, NfcMifareReadBlockParameter>(readBlockParameter);
var list = new List<NfcMifareReadBlockParameter>(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>(),
};
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<NfcMifareReadBlockData>())), 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<byte, NfcMifareWriteBlockParameter>(writeBlockParameter);
var list = new List<NfcMifareWriteBlockParameter>(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<TagInfo>());
MemoryHelper.FillWithZeros(context.Memory, outputPosition, Marshal.SizeOf<TagInfo>());
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<byte>(),
Protocol = (uint)NfcProtocol.NfcProtocol_TypeA, // Type A Protocol
TagType = (uint)NfcTagType.NfcTagType_Mifare, // Mifare Type
Reserved2 = new Array6<byte>(),
};
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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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,
}
}

View File

@@ -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<byte> Data;
public byte SectorNumber;
public Array7<byte> Reserved;
}
}

View File

@@ -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<byte> Reserved;
public NfcSectorKey SectorKey;
}
}

View File

@@ -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<byte> Data;
public byte SectorNumber;
public Array7<byte> Reserved;
public NfcSectorKey SectorKey;
}
}

View File

@@ -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,
}
}

View File

@@ -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<byte> Reserved1;
public Array6<byte> SectorKey;
public Array2<byte> Reserved2;
}
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -0,0 +1,8 @@
namespace Ryujinx.HLE.HOS.Services.Nfc.Mifare.MifareManager
{
enum State
{
NonInitialized,
Initialized,
}
}

View File

@@ -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<byte> Uuid;
public byte UuidLength;
public Array21<byte> Reserved1;
public uint Protocol;
public uint TagType;
public Array6<byte> Reserved2;
}
}

View File

@@ -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
}
}

View File

@@ -9,10 +9,20 @@ using static SDL.SDL3;
namespace Ryujinx.Input.SDL3
{
public unsafe class SDL3GamepadDriver : IGamepadDriver
{
private readonly Dictionary<SDL_JoystickID, string> _gamepadsInstanceIdsMapping;
private readonly Dictionary<SDL_JoystickID, string> _gamepadsIds;
/// <summary>
/// Unlinked joy-cons
/// </summary>
private readonly Dictionary<SDL_JoystickID, string> _joyConsIds;
/// <summary>
/// Linked joy-cons, remove dual joy-con from <c>_gamepadsIds</c> when a linked joy-con is removed
/// </summary>
private readonly Dictionary<SDL_JoystickID,string> _linkedJoyConsIds;
private readonly Lock _lock = new();
public ReadOnlySpan<string> GamepadsIds
@@ -21,7 +31,11 @@ namespace Ryujinx.Input.SDL3
{
lock (_lock)
{
return _gamepadsIds.Values.ToArray();
List<string> temp = [];
temp.AddRange(_gamepadsIds.Values);
temp.AddRange(_joyConsIds.Values);
temp.AddRange(_linkedJoyConsIds.Values);
return temp.ToArray();
}
}
}
@@ -35,6 +49,8 @@ namespace Ryujinx.Input.SDL3
{
_gamepadsInstanceIdsMapping = new Dictionary<SDL_JoystickID, string>();
_gamepadsIds = [];
_joyConsIds = [];
_linkedJoyConsIds = [];
SDL3Driver.Instance.Initialize();
SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
@@ -92,7 +108,7 @@ namespace Ryujinx.Input.SDL3
int guidIndex = 0;
id = guidIndex + "-" + guidString;
while (_gamepadsIds.ContainsValue(id))
while (_gamepadsIds.ContainsValue(id) || _joyConsIds.ContainsValue(id) || _linkedJoyConsIds.ContainsValue(id))
{
id = (++guidIndex) + "-" + guidString;
}
@@ -104,16 +120,47 @@ namespace Ryujinx.Input.SDL3
private void HandleJoyStickDisconnected(SDL_JoystickID joystickInstanceId)
{
bool joyConPairDisconnected = false;
string fakeId = null;
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
return;
lock (_lock)
{
_gamepadsIds.Remove(joystickInstanceId);
if (!SDL3JoyConPair.IsCombinable(_gamepadsIds))
if (!_linkedJoyConsIds.ContainsKey(joystickInstanceId))
{
_gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id));
if (!_joyConsIds.Remove(joystickInstanceId))
{
_gamepadsIds.Remove(joystickInstanceId);
}
}
else
{
foreach (string matchId in _gamepadsIds.Values)
{
if (matchId.Contains(id))
{
fakeId = matchId;
break;
}
}
string leftId = fakeId!.Split('_')[0];
string rightId = fakeId!.Split('_')[1];
if (leftId == id)
{
_linkedJoyConsIds.Remove(GetInstanceIdFromId(rightId));
_joyConsIds.Add(GetInstanceIdFromId(rightId), rightId);
}
else
{
_linkedJoyConsIds.Remove(GetInstanceIdFromId(leftId));
_joyConsIds.Add(GetInstanceIdFromId(leftId), leftId);
}
_linkedJoyConsIds.Remove(joystickInstanceId);
_gamepadsIds.Remove(GetInstanceIdFromId(fakeId));
joyConPairDisconnected = true;
}
}
@@ -121,13 +168,14 @@ namespace Ryujinx.Input.SDL3
OnGamepadDisconnected?.Invoke(id);
if (joyConPairDisconnected)
{
OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id);
OnGamepadDisconnected?.Invoke(fakeId);
}
}
private void HandleJoyStickConnected(SDL_JoystickID joystickInstanceId)
{
bool joyConPairConnected = false;
string fakeId = null;
if (SDL_IsGamepad(joystickInstanceId))
{
@@ -149,27 +197,40 @@ namespace Ryujinx.Input.SDL3
{
lock (_lock)
{
_gamepadsIds.Add(joystickInstanceId, id);
if (SDL3JoyConPair.IsCombinable(_gamepadsIds))
if (!SDL3JoyCon.IsJoyCon(joystickInstanceId))
{
// TODO - It appears that you can only have one joy con pair connected at a time?
// This was also the behavior before SDL3
_gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id));
uint fakeInstanceID = uint.MaxValue;
while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceID, SDL3JoyConPair.Id))
_gamepadsIds.Add(joystickInstanceId, id);
}
else
{
if (SDL3JoyConPair.IsCombinable(joystickInstanceId, _joyConsIds, out SDL_JoystickID match))
{
fakeInstanceID--;
_joyConsIds.Remove(match, out string matchId);
_linkedJoyConsIds.Add(joystickInstanceId, id);
_linkedJoyConsIds.Add(match, matchId);
uint fakeInstanceId = uint.MaxValue;
fakeId = SDL3JoyCon.IsLeftJoyCon(joystickInstanceId)
? $"{id}_{matchId}"
: $"{matchId}_{id}";
while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceId, fakeId))
{
fakeInstanceId--;
}
_gamepadsInstanceIdsMapping.Add((SDL_JoystickID)fakeInstanceId, fakeId);
joyConPairConnected = true;
}
else
{
_joyConsIds.Add(joystickInstanceId, id);
}
joyConPairConnected = true;
}
}
OnGamepadConnected?.Invoke(id);
if (joyConPairConnected)
{
OnGamepadConnected?.Invoke(SDL3JoyConPair.Id);
OnGamepadConnected?.Invoke(fakeId);
}
}
}
@@ -193,10 +254,22 @@ namespace Ryujinx.Input.SDL3
{
OnGamepadDisconnected?.Invoke(gamepad.Value);
}
foreach (var gamepad in _joyConsIds)
{
OnGamepadDisconnected?.Invoke(gamepad.Value);
}
foreach (var gamepad in _linkedJoyConsIds)
{
OnGamepadDisconnected?.Invoke(gamepad.Value);
}
lock (_lock)
{
_gamepadsIds.Clear();
_joyConsIds.Clear();
_linkedJoyConsIds.Clear();
}
SDL3Driver.Instance.Dispose();
@@ -215,11 +288,27 @@ namespace Ryujinx.Input.SDL3
public IGamepad GetGamepad(string id)
{
if (id == SDL3JoyConPair.Id)
// joy-con pair ids is the combined ids of its parts which are split using a '_'
if (id.Contains('_'))
{
lock (_lock)
{
return SDL3JoyConPair.GetGamepad(_gamepadsIds);
string leftId = id.Split('_')[0];
string rightId = id.Split('_')[1];
SDL_JoystickID leftInstanceId = GetInstanceIdFromId(leftId);
SDL_JoystickID rightInstanceId = GetInstanceIdFromId(rightId);
SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad(leftInstanceId);
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad(rightInstanceId);
if (leftGamepadHandle == null || rightGamepadHandle == null)
{
return null;
}
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, leftId),
new SDL3JoyCon(rightGamepadHandle, rightId));
}
}
@@ -232,7 +321,7 @@ namespace Ryujinx.Input.SDL3
return null;
}
if (SDL_GetGamepadName(gamepadHandle).StartsWith(SDL3JoyCon.Prefix))
if (SDL3JoyCon.IsJoyCon(instanceId))
{
return new SDL3JoyCon(gamepadHandle, id);
}
@@ -249,6 +338,22 @@ namespace Ryujinx.Input.SDL3
yield return GetGamepad(gamepad.Value);
}
}
lock (_joyConsIds)
{
foreach (var gamepad in _joyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
lock (_linkedJoyConsIds)
{
foreach (var gamepad in _linkedJoyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
}
}
}

View File

@@ -24,10 +24,10 @@ namespace Ryujinx.Input.SDL3
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _leftButtonsDriverMapping = new()
{
{GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK},
{GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
{GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
{GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
{GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
{GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
{GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
{GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
{GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
{GamepadButtonInputId.Minus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START},
{GamepadButtonInputId.LeftShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
{GamepadButtonInputId.LeftTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
@@ -37,10 +37,10 @@ namespace Ryujinx.Input.SDL3
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _rightButtonsDriverMapping = new()
{
{GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK},
{GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
{GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
{GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
{GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
{GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
{GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
{GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
{GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
{GamepadButtonInputId.Plus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START},
{GamepadButtonInputId.RightShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
{GamepadButtonInputId.RightTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
@@ -398,5 +398,15 @@ namespace Ryujinx.Input.SDL3
return SDL_GetGamepadButton(_gamepadHandle, button);
}
public static bool IsJoyCon(SDL_JoystickID gamepadsId)
{
return SDL_GetGamepadNameForID(gamepadsId) is LeftName or RightName;
}
public static bool IsLeftJoyCon(SDL_JoystickID gamepadsId)
{
return SDL_GetGamepadNameForID(gamepadsId) is LeftName;
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Ryujinx.Input.SDL3
public const string Id = "JoyConPair";
string IGamepad.Id => Id;
public string Name => "* Nintendo Switch Joy-Con (L/R)";
public string Name => "Nintendo Switch Dual Joy-Con (L/R)";
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
public void Dispose()
@@ -96,44 +96,23 @@ namespace Ryujinx.Input.SDL3
right.SetTriggerThreshold(triggerThreshold);
}
public static bool IsCombinable(Dictionary<SDL_JoystickID, string> gamepadsIds)
public static bool IsCombinable(SDL_JoystickID joyCon1, Dictionary<SDL_JoystickID, string> joyConIds, out SDL_JoystickID match)
{
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
return leftIndex >= 0 && rightIndex >= 0;
}
bool isLeft = SDL3JoyCon.IsLeftJoyCon(joyCon1);
string matchName = isLeft ? SDL3JoyCon.RightName : SDL3JoyCon.LeftName;
match = 0;
private static (int leftIndex, int rightIndex) DetectJoyConPair(Dictionary<SDL_JoystickID, string> gamepadsIds)
{
Dictionary<string, SDL_JoystickID> gamepadNames = gamepadsIds
.Where(gamepadId => gamepadId.Value != Id && SDL_GetGamepadNameForID(gamepadId.Key) is SDL3JoyCon.LeftName or SDL3JoyCon.RightName)
.Select(gamepad => (SDL_GetGamepadNameForID(gamepad.Key), gamepad.Key))
.ToDictionary();
SDL_JoystickID idx;
int leftIndex = gamepadNames.TryGetValue(SDL3JoyCon.LeftName, out idx) ? (int)idx : -1;
int rightIndex = gamepadNames.TryGetValue(SDL3JoyCon.RightName, out idx) ? (int)idx : -1;
return (leftIndex, rightIndex);
}
public unsafe static IGamepad GetGamepad(Dictionary<SDL_JoystickID, string> gamepadsIds)
{
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
if (leftIndex <= 0 || rightIndex <= 0)
foreach (var joyConId in joyConIds.Keys)
{
return null;
if (SDL_GetGamepadNameForID(joyConId) == matchName)
{
match = joyConId;
return true;
}
}
SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)leftIndex);
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)rightIndex);
if (leftGamepadHandle == null || rightGamepadHandle == null)
{
return null;
}
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, gamepadsIds[(SDL_JoystickID)leftIndex]),
new SDL3JoyCon(rightGamepadHandle, gamepadsIds[(SDL_JoystickID)rightIndex]));
return false;
}
}
}

View File

@@ -141,7 +141,7 @@ namespace Ryujinx.Ava.Input
AvaKey.OemComma,
AvaKey.OemPeriod,
AvaKey.OemQuestion,
AvaKey.OemBackslash,
AvaKey.OemPipe,
// NOTE: invalid
AvaKey.None

View File

@@ -184,7 +184,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_controller = 0;
}
if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1)
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
{
ControllerType controller = Controllers[_controller].Type;
@@ -467,7 +467,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
IsModified = true;
RevertChanges();
FindPairedDeviceInConfigFile();
_isChangeTrackingActive = true; // Enable configuration change tracking
}
@@ -521,7 +521,17 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1)
{
Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
int controllerIndex = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
// Avalonia bug: setting a newly instanced ComboBox to 0
// causes the selected item to show up blank
// Workaround: set the box to 1 and then 0
if (controllerIndex == 0)
{
Controller = 1;
}
Controller = controllerIndex;
}
else
{
@@ -576,7 +586,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
DeviceList.Clear();
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
int controllerNumber = 0;
foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds)
{
using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
@@ -593,6 +603,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (gamepad != null)
{
int controllerNumber = 0;
string name = GetUniqueGamepadName(gamepad, ref controllerNumber);
Devices.Add((DeviceType.Controller, id, name));
}
@@ -950,8 +961,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad
Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId);
LoadDevice();
_isLoaded = false;
LoadConfiguration();
LoadDevice();
_isLoaded = true;
OnPropertyChanged();
IsModified = false;

View File

@@ -370,6 +370,39 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool CanScanAmiiboBinaries => AmiiboBinReader.HasAmiiboKeyFile;
public bool IsSkylanderRequested
{
get => field && _isGameRunning;
set
{
field = value;
OnPropertyChanged();
}
}
public bool HasSkylander
{
get => field && _isGameRunning;
set
{
field = value;
OnPropertyChanged();
}
}
public bool ShowSkylanderActions
{
get => field && _isGameRunning;
set
{
field = value;
OnPropertyChanged();
}
}
public bool ShowLoadProgress
{
get;
@@ -1864,6 +1897,46 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
}
public async Task OpenSkylanderWindow()
{
if (AppHost.Device.System.SearchingForSkylander(out int deviceId))
{
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(
new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
{
Patterns = ["*.sky", "*.bin", "*.dmp", "*.dump"],
},
},
});
if (result.HasValue)
{
// Open reading stream from the first file.
await using var stream = await result.Value.OpenReadAsync();
using var streamReader = new BinaryReader(stream);
// Reads all the content of file as a text.
byte[] data = new byte[1024];
var count = streamReader.Read(data, 0, 1024);
if (count < 1024)
{
return;
}
else
{
AppHost.Device.System.ScanSkylander(deviceId, data);
}
}
}
}
public async Task RemoveSkylander()
{
AppHost.Device.System.RemoveSkylander();
}
public void ReloadRenderDocApi()
{

View File

@@ -184,6 +184,22 @@
IsVisible="{Binding CanScanAmiiboBinaries}"
InputGesture="Ctrl + B"
IsEnabled="{Binding IsAmiiboBinRequested}" />
<MenuItem
Command="{Binding OpenSkylanderWindow}"
AttachedToVisualTree="ScanSkylanderMenuItem_AttachedToVisualTree"
Header="{ext:Locale MenuBarActionsScanSkylander}"
Icon="{ext:Icon fa-solid fa-cube}"
IsVisible="{Binding ShowSkylanderActions}"
InputGesture="Ctrl + S"
IsEnabled="{Binding IsSkylanderRequested}" />
<MenuItem
Command="{Binding RemoveSkylander}"
AttachedToVisualTree="RemoveSkylanderMenuItem_AttachedToVisualTree"
Header="{ext:Locale MenuBarActionsRemoveSkylander}"
Icon="{ext:Icon fa-solid fa-cube}"
IsVisible="{Binding ShowSkylanderActions}"
InputGesture="Ctrl + D"
IsEnabled="{Binding HasSkylander}" />
<MenuItem
Command="{Binding TakeScreenshot}"
Header="{ext:Locale MenuBarFileToolsTakeScreenshot}"

View File

@@ -193,6 +193,20 @@ namespace Ryujinx.Ava.UI.Views.Main
ViewModel.IsAmiiboBinRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasAmiiboKeyFile;
}
private void ScanSkylanderMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is MenuItem)
ViewModel.IsSkylanderRequested = ViewModel.AppHost.Device.System.SearchingForSkylander(out _);
ViewModel.ShowSkylanderActions = string.Equals(ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(), "0100CCC0002E6000");
}
private void RemoveSkylanderMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is MenuItem)
ViewModel.HasSkylander = ViewModel.AppHost.Device.System.HasSkylander(out _);
ViewModel.ShowSkylanderActions = string.Equals(ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(), "0100CCC0002E6000");
}
private async Task InstallFileTypes()
{
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();