diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs index a328691..6668fed 100644 --- a/src/SharpEmu.Libs/Pad/PadExports.cs +++ b/src/SharpEmu.Libs/Pad/PadExports.cs @@ -24,10 +24,17 @@ public static class PadExports private const int ControllerInformationSize = 0x1C; private const int PadDataSize = 0x78; - // Real firmware hands out small non-negative handles; 0 is valid. Some titles - // (Monster Truck Championship) read pad state with handle 0, and rejecting it - // leaves their controller/FFB init path polling a never-valid state forever. - private static bool IsPrimaryPadHandle(int handle) => handle is 0 or PrimaryPadHandle; + // Monster Truck Championship reads pad state with handle 0 before it opens a pad, + // and rejecting that leaves its controller/FFB init path polling forever. After a + // title opens a pad, handle 0 is a slot it never opened: Unreal Engine polls all four + // user slots and treats every valid handle as a connected controller, so accepting + // 0 there reports phantom pads that repeat the primary pad's buttons. + private static int _padOpened; + + private static bool IsPrimaryPadHandle(int handle) => + handle == PrimaryPadHandle || (handle == 0 && Volatile.Read(ref _padOpened) == 0); + + internal static void ResetOpenedPadForTests() => Volatile.Write(ref _padOpened, 0); private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000); [ThreadStatic] @@ -128,6 +135,7 @@ public static class PadExports : "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in."); } + Volatile.Write(ref _padOpened, 1); return ctx.SetReturn(PrimaryPadHandle); } diff --git a/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs b/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs index 5cf8852..df447c3 100644 --- a/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs @@ -73,4 +73,31 @@ public sealed class PadExportsTests _ctx[CpuRegister.Rsi] = Base + 0x100; Assert.Equal(InvalidHandle, PadExports.PadGetTriggerEffectState(_ctx)); } + + [NativeX64Fact] + public void ReadState_RejectsHandleZeroOnceAPadIsOpen() + { + const ulong dataAddress = Base + 0x200; + try + { + PadExports.PadInit(_ctx); + _ctx[CpuRegister.Rdi] = 0x10000000; + _ctx[CpuRegister.Rsi] = 0; + _ctx[CpuRegister.Rdx] = 0; + _ctx[CpuRegister.Rcx] = 0; + Assert.Equal(1, PadExports.PadOpen(_ctx)); + + _ctx[CpuRegister.Rdi] = 0; + _ctx[CpuRegister.Rsi] = dataAddress; + Assert.Equal(InvalidHandle, PadExports.PadReadState(_ctx)); + + _ctx[CpuRegister.Rdi] = 1; + _ctx[CpuRegister.Rsi] = dataAddress; + Assert.Equal(0, PadExports.PadReadState(_ctx)); + } + finally + { + PadExports.ResetOpenedPadForTests(); + } + } }