Explorar o código

Implement IApplicationFunctions & IQueryService commands (#823)

* Implement IApplicationFunctions & IQueryService commands

- Fix some nits in `IApplicationFunctions`
- Implement `QueryApplicationPlayStatistics` and `QueryApplicationPlayStatisticsByUid` checked by RE.
- Implement `QueryApplicationPlayStatisticsForSystem` and `QueryApplicationPlayStatisticsByUserAccountIdForSystem` checked by RE.
- Implement `QueryPlayStatisticsManager` to get/set played games statistics. We currently don't store any statistics because it's handled by qlaunch (or maybe am service?) on Switch.
We can add support later if games use returned statistics for something.

* Fix reviews
Ac_K %!s(int64=6) %!d(string=hai) anos
pai
achega
79abc6ed93

+ 26 - 10
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.Kernel.Threading;
 using Ryujinx.HLE.HOS.Services.Am.AppletAE;
 using Ryujinx.HLE.HOS.Services.Am.AppletAE;
 using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
 using Ryujinx.HLE.HOS.Services.Am.AppletAE.Storage;
+using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService;
+using Ryujinx.HLE.Utilities;
 using System;
 using System;
 
 
 namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy
 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
         // EnsureSaveData(nn::account::Uid) -> u64
         public ResultCode EnsureSaveData(ServiceCtx context)
         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);
             context.ResponseData.Write(0L);
 
 
+            Logger.PrintStub(LogClass.ServiceAm, new { userId });
+
             return ResultCode.Success;
             return ResultCode.Success;
         }
         }
 
 
@@ -54,9 +55,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
         // SetTerminateResult(u32)
         // SetTerminateResult(u32)
         public ResultCode SetTerminateResult(ServiceCtx context)
         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}).");
             Logger.PrintInfo(LogClass.ServiceAm, $"Result = 0x{errorCode:x8} ({result}).");
 
 
@@ -95,11 +95,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
         // GetPseudoDeviceId() -> nn::util::Uuid
         // GetPseudoDeviceId() -> nn::util::Uuid
         public ResultCode GetPseudoDeviceId(ServiceCtx context)
         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;
             return ResultCode.Success;
         }
         }
 
 
@@ -118,11 +118,27 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
         {
         {
             int state = context.RequestData.ReadInt32();
             int state = context.RequestData.ReadInt32();
 
 
-            Logger.PrintStub(LogClass.ServiceAm);
+            Logger.PrintStub(LogClass.ServiceAm, new { state });
 
 
             return ResultCode.Success;
             return ResultCode.Success;
         }
         }
 
 
+        [Command(110)] // 5.0.0+
+        // QueryApplicationPlayStatistics(buffer<bytes, 5> title_id_list) -> (buffer<bytes, 6> 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<bytes, 5> title_id_list) -> (buffer<bytes, 6> 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+
         [Command(130)] // 8.0.0+
         // GetGpuErrorDetectedSystemEvent() -> handle<copy>
         // GetGpuErrorDetectedSystemEvent() -> handle<copy>
         public ResultCode GetGpuErrorDetectedSystemEvent(ServiceCtx context)
         public ResultCode GetGpuErrorDetectedSystemEvent(ServiceCtx context)

+ 1 - 0
Ryujinx.HLE/HOS/Services/Am/ResultCode.cs

@@ -8,6 +8,7 @@ namespace Ryujinx.HLE.HOS.Services.Am
         Success = 0,
         Success = 0,
 
 
         NoMessages          = (3   << ErrorCodeShift) | ModuleId,
         NoMessages          = (3   << ErrorCodeShift) | ModuleId,
+        ObjectInvalid       = (500 << ErrorCodeShift) | ModuleId,
         CpuBoostModeInvalid = (506 << ErrorCodeShift) | ModuleId
         CpuBoostModeInvalid = (506 << ErrorCodeShift) | ModuleId
     }
     }
 }
 }

+ 17 - 1
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")]
     [Service("pdm:qry")]
     class IQueryService : IpcService
     class IQueryService : IpcService
     {
     {
         public IQueryService(ServiceCtx context) { }
         public IQueryService(ServiceCtx context) { }
+
+        [Command(13)] // 5.0.0+
+        // QueryApplicationPlayStatisticsForSystem(buffer<bytes, 5> title_id_list) -> (buffer<bytes, 6> entries, s32 entries_count)
+        public ResultCode QueryApplicationPlayStatisticsForSystem(ServiceCtx context)
+        {
+            return QueryPlayStatisticsManager.GetPlayStatistics(context);
+        }
+
+        [Command(16)] // 6.0.0+
+        // QueryApplicationPlayStatisticsByUserAccountIdForSystem(nn::account::Uid, buffer<bytes, 5> title_id_list) -> (buffer<bytes, 6> entries, s32 entries_count)
+        public ResultCode QueryApplicationPlayStatisticsByUserAccountIdForSystem(ServiceCtx context)
+        {
+            return QueryPlayStatisticsManager.GetPlayStatistics(context, true);
+        }
     }
     }
 }
 }

+ 83 - 0
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<UInt128, ApplicationPlayStatistics> applicationPlayStatistics = new Dictionary<UInt128, ApplicationPlayStatistics>();
+
+        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<ulong> titleIds = new List<ulong>();
+
+            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<ApplicationPlayStatistics>()), filteredApplicationPlayStatistics.ElementAt(i).Value);
+            }
+
+            context.ResponseData.Write(filteredApplicationPlayStatistics.Count());
+
+            return ResultCode.Success;
+        }
+    }
+}

+ 12 - 0
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;
+    }
+}

+ 9 - 0
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
+    }
+}

+ 13 - 0
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
+    }
+}