diff --git a/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs index f65509b5..464d0b47 100644 --- a/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs +++ b/Ryujinx.HLE/HOS/Services/Am/AppletOE/ApplicationProxyService/ApplicationProxy/IApplicationFunctions.cs @@ -4,6 +4,8 @@ using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Services.Am.AppletAE; using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage; +using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService; +using Ryujinx.HLE.Utilities; using System; namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy @@ -31,13 +33,12 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati // EnsureSaveData(nn::account::Uid) -> u64 public ResultCode EnsureSaveData(ServiceCtx context) { - long uIdLow = context.RequestData.ReadInt64(); - long uIdHigh = context.RequestData.ReadInt64(); - - Logger.PrintStub(LogClass.ServiceAm); + UInt128 userId = new UInt128(context.RequestData.ReadBytes(0x10)); context.ResponseData.Write(0L); + Logger.PrintStub(LogClass.ServiceAm, new { userId }); + return ResultCode.Success; } @@ -54,9 +55,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati // SetTerminateResult(u32) public ResultCode SetTerminateResult(ServiceCtx context) { - int errorCode = context.RequestData.ReadInt32(); - - string result = GetFormattedErrorCode(errorCode); + int errorCode = context.RequestData.ReadInt32(); + string result = GetFormattedErrorCode(errorCode); Logger.PrintInfo(LogClass.ServiceAm, $"Result = 0x{errorCode:x8} ({result})."); @@ -95,10 +95,10 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati // GetPseudoDeviceId() -> nn::util::Uuid public ResultCode GetPseudoDeviceId(ServiceCtx context) { - Logger.PrintStub(LogClass.ServiceAm); + context.ResponseData.Write(0L); + context.ResponseData.Write(0L); - context.ResponseData.Write(0L); - context.ResponseData.Write(0L); + Logger.PrintStub(LogClass.ServiceAm); return ResultCode.Success; } @@ -118,11 +118,27 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati { int state = context.RequestData.ReadInt32(); - Logger.PrintStub(LogClass.ServiceAm); + Logger.PrintStub(LogClass.ServiceAm, new { state }); return ResultCode.Success; } + [Command(110)] // 5.0.0+ + // QueryApplicationPlayStatistics(buffer title_id_list) -> (buffer entries, s32 entries_count) + public ResultCode QueryApplicationPlayStatistics(ServiceCtx context) + { + // TODO: Call pdm:qry cmd 13 when IPC call between services will be implemented. + return (ResultCode)QueryPlayStatisticsManager.GetPlayStatistics(context); + } + + [Command(111)] // 6.0.0+ + // QueryApplicationPlayStatisticsByUid(nn::account::Uid, buffer title_id_list) -> (buffer entries, s32 entries_count) + public ResultCode QueryApplicationPlayStatisticsByUid(ServiceCtx context) + { + // TODO: Call pdm:qry cmd 16 when IPC call between services will be implemented. + return (ResultCode)QueryPlayStatisticsManager.GetPlayStatistics(context, true); + } + [Command(130)] // 8.0.0+ // GetGpuErrorDetectedSystemEvent() -> handle public ResultCode GetGpuErrorDetectedSystemEvent(ServiceCtx context) diff --git a/Ryujinx.HLE/HOS/Services/Am/ResultCode.cs b/Ryujinx.HLE/HOS/Services/Am/ResultCode.cs index b46bd2b3..a5eb42f3 100644 --- a/Ryujinx.HLE/HOS/Services/Am/ResultCode.cs +++ b/Ryujinx.HLE/HOS/Services/Am/ResultCode.cs @@ -8,6 +8,7 @@ namespace Ryujinx.HLE.HOS.Services.Am Success = 0, NoMessages = (3 << ErrorCodeShift) | ModuleId, + ObjectInvalid = (500 << ErrorCodeShift) | ModuleId, CpuBoostModeInvalid = (506 << ErrorCodeShift) | ModuleId } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Sdb/Pdm/IQueryService.cs b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/IQueryService.cs index 61b26b8c..210dd98b 100644 --- a/Ryujinx.HLE/HOS/Services/Sdb/Pdm/IQueryService.cs +++ b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/IQueryService.cs @@ -1,8 +1,24 @@ -namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm +using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService; + +namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm { [Service("pdm:qry")] class IQueryService : IpcService { public IQueryService(ServiceCtx context) { } + + [Command(13)] // 5.0.0+ + // QueryApplicationPlayStatisticsForSystem(buffer title_id_list) -> (buffer entries, s32 entries_count) + public ResultCode QueryApplicationPlayStatisticsForSystem(ServiceCtx context) + { + return QueryPlayStatisticsManager.GetPlayStatistics(context); + } + + [Command(16)] // 6.0.0+ + // QueryApplicationPlayStatisticsByUserAccountIdForSystem(nn::account::Uid, buffer title_id_list) -> (buffer entries, s32 entries_count) + public ResultCode QueryApplicationPlayStatisticsByUserAccountIdForSystem(ServiceCtx context) + { + return QueryPlayStatisticsManager.GetPlayStatistics(context, true); + } } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/QueryPlayStatisticsManager.cs b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/QueryPlayStatisticsManager.cs new file mode 100644 index 00000000..b3646925 --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/QueryPlayStatisticsManager.cs @@ -0,0 +1,83 @@ +using ARMeilleure.Memory; +using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService +{ + static class QueryPlayStatisticsManager + { + private static Dictionary applicationPlayStatistics = new Dictionary(); + + internal static ResultCode GetPlayStatistics(ServiceCtx context, bool byUserId = false) + { + long inputPosition = context.Request.SendBuff[0].Position; + long inputSize = context.Request.SendBuff[0].Size; + + long outputPosition = context.Request.ReceiveBuff[0].Position; + long outputSize = context.Request.ReceiveBuff[0].Size; + + UInt128 userId = byUserId ? new UInt128(context.RequestData.ReadBytes(0x10)) : new UInt128(); + + if (byUserId) + { + if (!context.Device.System.State.Account.TryGetUser(userId, out _)) + { + return ResultCode.UserNotFound; + } + } + + PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.System.ControlData.PlayLogQueryCapability; + + List titleIds = new List(); + + for (int i = 0; i < inputSize / sizeof(ulong); i++) + { + titleIds.Add(BitConverter.ToUInt64(context.Memory.ReadBytes(inputPosition, inputSize), 0)); + } + + if (queryCapability == PlayLogQueryCapability.WhiteList) + { + // Check if input title ids are in the whitelist. + foreach (ulong titleId in titleIds) + { + if (!context.Device.System.ControlData.PlayLogQueryableApplicationId.Contains(titleId)) + { + return (ResultCode)Am.ResultCode.ObjectInvalid; + } + } + } + + MemoryHelper.FillWithZeros(context.Memory, outputPosition, (int)outputSize); + + // Return ResultCode.ServiceUnavailable if data is locked by another process. + var filteredApplicationPlayStatistics = applicationPlayStatistics.AsEnumerable(); + + if (queryCapability == PlayLogQueryCapability.None) + { + filteredApplicationPlayStatistics = filteredApplicationPlayStatistics.Where(kv => kv.Value.TitleId == context.Process.TitleId); + } + else // PlayLogQueryCapability.All + { + filteredApplicationPlayStatistics = filteredApplicationPlayStatistics.Where(kv => titleIds.Contains(kv.Value.TitleId)); + } + + if (byUserId) + { + filteredApplicationPlayStatistics = filteredApplicationPlayStatistics.Where(kv => kv.Key == userId); + } + + for (int i = 0; i < filteredApplicationPlayStatistics.Count(); i++) + { + MemoryHelper.Write(context.Memory, outputPosition + (i * Marshal.SizeOf()), filteredApplicationPlayStatistics.ElementAt(i).Value); + } + + context.ResponseData.Write(filteredApplicationPlayStatistics.Count()); + + return ResultCode.Success; + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/Types/ApplicationPlayStatistics.cs b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/Types/ApplicationPlayStatistics.cs new file mode 100644 index 00000000..c28d757e --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/Types/ApplicationPlayStatistics.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18)] + struct ApplicationPlayStatistics + { + public ulong TitleId; + public long TotalPlayTime; // In nanoseconds. + public long TotalLaunchCount; + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/Types/PlayLogQueryCapability.cs b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/Types/PlayLogQueryCapability.cs new file mode 100644 index 00000000..9e4b85de --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/QueryService/Types/PlayLogQueryCapability.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types +{ + enum PlayLogQueryCapability + { + None, + WhiteList, + All + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Sdb/Pdm/ResultCode.cs b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/ResultCode.cs new file mode 100644 index 00000000..3ceb8d1a --- /dev/null +++ b/Ryujinx.HLE/HOS/Services/Sdb/Pdm/ResultCode.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm +{ + enum ResultCode + { + ModuleId = 178, + ErrorCodeShift = 9, + + Success = 0, + + UserNotFound = (101 << ErrorCodeShift) | ModuleId, + ServiceUnavailable = (150 << ErrorCodeShift) | ModuleId + } +} \ No newline at end of file