diff --git a/src/SharpEmu.Libs/Pad/PadExports.cs b/src/SharpEmu.Libs/Pad/PadExports.cs index 6db7e017..a328691c 100644 --- a/src/SharpEmu.Libs/Pad/PadExports.cs +++ b/src/SharpEmu.Libs/Pad/PadExports.cs @@ -373,6 +373,42 @@ public static class PadExports return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK); } + // Size taken from the caller's own frame rather than assumed: the guest + // reserves 0x10 bytes, points the out-param at rbp-0x30, and stores its + // stack cookie at rbp-0x28, so only eight bytes belong to the state. A + // sixteen-byte write would land on the cookie and fail the stack check. + private const int TriggerEffectStateSize = 8; + + [SysAbiExport( + Nid = "znaWI0gpuo8", + ExportName = "scePadGetTriggerEffectState", + Target = Generation.Gen4 | Generation.Gen5, + LibraryName = "libScePad")] + public static int PadGetTriggerEffectState(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var stateAddress = ctx[CpuRegister.Rsi]; + if (!IsPrimaryPadHandle(handle)) + { + return ctx.SetReturn(OrbisPadErrorInvalidHandle); + } + + if (stateAddress == 0) + { + return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + // No host pad exposes DualSense adaptive-trigger feedback, so every + // trigger reports the neutral "no effect engaged" state. Reporting it + // as success is what lets the caller take its normal path instead of + // falling back to a cached button bitmask every poll. + Span state = stackalloc byte[TriggerEffectStateSize]; + state.Clear(); + return ctx.Memory.TryWrite(stateAddress, state) + ? ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK) + : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + } + private static HostAdaptiveTriggerEffect DecodeTriggerEffect(ReadOnlySpan command) { var mode = BinaryPrimitives.ReadUInt32LittleEndian(command); diff --git a/src/SharpEmu.Libs/UserService/UserServiceExports.cs b/src/SharpEmu.Libs/UserService/UserServiceExports.cs index f395d51c..e4cd3458 100644 --- a/src/SharpEmu.Libs/UserService/UserServiceExports.cs +++ b/src/SharpEmu.Libs/UserService/UserServiceExports.cs @@ -144,16 +144,6 @@ public static class UserServiceExports : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } - // Title-captured alias NID for the same username query. - #pragma warning disable SHEM004 - [SysAbiExport( - Nid = "znaWI0gpuo8", - ExportName = "sceUserServiceGetUserName", - Target = Generation.Gen4 | Generation.Gen5, - LibraryName = "libSceUserService")] - public static int UserServiceGetUserNameAlt(CpuContext ctx) => UserServiceGetUserName(ctx); - #pragma warning restore SHEM004 - // Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued. #pragma warning disable SHEM006 [SysAbiExport( diff --git a/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs b/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs index ba01238b..5cf88521 100644 --- a/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs @@ -30,4 +30,47 @@ public sealed class PadExportsTests _ctx[CpuRegister.Rdi] = unchecked((ulong)handle); Assert.Equal(expected, PadExports.PadSetTiltCorrectionState(_ctx)); } + + /// + /// Mirrors the calling frame observed in PPSA10112: the out-param points at + /// rbp-0x30 and the caller's stack cookie sits at rbp-0x28, so the state is + /// eight bytes. Writing more would smash the cookie and fail the guest's + /// stack check, which is the failure mode this size guards against. + /// + [Fact] + public void GetTriggerEffectState_WritesEightBytesAndLeavesTheCookieIntact() + { + const ulong stateAddress = Base + 0x100; + const ulong cookieAddress = stateAddress + 8; + const ulong cookie = 0xC0DEC0DECAFEBA00UL; + + Assert.True(_memory.TryWrite(stateAddress, new byte[] { 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE })); + Assert.True(_memory.TryWrite(cookieAddress, BitConverter.GetBytes(cookie))); + + _ctx[CpuRegister.Rdi] = 0; + _ctx[CpuRegister.Rsi] = stateAddress; + + Assert.Equal(0, PadExports.PadGetTriggerEffectState(_ctx)); + + Span state = stackalloc byte[8]; + Assert.True(_memory.TryRead(stateAddress, state)); + foreach (var value in state) + { + Assert.Equal(0, value); + } + + Span guard = stackalloc byte[8]; + Assert.True(_memory.TryRead(cookieAddress, guard)); + Assert.Equal(cookie, BitConverter.ToUInt64(guard)); + } + + [Theory] + [InlineData(2)] + [InlineData(-1)] + public void GetTriggerEffectState_RejectsForeignHandles(int handle) + { + _ctx[CpuRegister.Rdi] = unchecked((ulong)handle); + _ctx[CpuRegister.Rsi] = Base + 0x100; + Assert.Equal(InvalidHandle, PadExports.PadGetTriggerEffectState(_ctx)); + } }