Browse Source

UI: Play Report Analysis V2
Support for multiple keys per game, and provide an order of resolution via Priority.

(Currently) functionally identical to before, as only BOTW Master Mode is supported.

Evan Husted 1 year ago
parent
commit
2d7700949c

+ 80 - 0
src/Ryujinx.Common/Helpers/PlayReportAnalyzer.cs

@@ -0,0 +1,80 @@
+using Gommon;
+using MsgPack;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Common.Helper
+{
+    public class PlayReportAnalyzer
+    {
+        private readonly List<PlayReportGameSpec> _specs = [];
+
+        public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
+        {
+            _specs.Add(transform(new PlayReportGameSpec { TitleIdStr = titleId }));
+            return this;
+        }
+        
+        public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
+        {
+            _specs.Add(new PlayReportGameSpec { TitleIdStr = titleId }.Apply(transform));
+            return this;
+        }
+
+        public Optional<string> Run(string runningGameId, MessagePackObject playReport)
+        {
+            if (!playReport.IsDictionary) 
+                return Optional<string>.None;
+
+            if (!_specs.TryGetFirst(s => s.TitleIdStr.EqualsIgnoreCase(runningGameId), out PlayReportGameSpec spec))
+                return Optional<string>.None;
+
+            foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority))
+            {
+                if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
+                    continue;
+
+                return formatSpec.ValueFormatter(valuePackObject.ToObject());
+            }
+            
+            return Optional<string>.None;
+        }
+        
+    }
+
+    public class PlayReportGameSpec
+    {
+        public required string TitleIdStr { get; init; }
+        public List<PlayReportValueFormatterSpec> Analyses { get; } = [];
+
+        public PlayReportGameSpec AddValueFormatter(string reportKey, Func<object, string> valueFormatter)
+        {
+            Analyses.Add(new PlayReportValueFormatterSpec
+            {
+                Priority = Analyses.Count,
+                ReportKey = reportKey, 
+                ValueFormatter = valueFormatter
+            });
+            return this;
+        }
+        
+        public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, Func<object, string> valueFormatter)
+        {
+            Analyses.Add(new PlayReportValueFormatterSpec
+            {
+                Priority = priority,
+                ReportKey = reportKey, 
+                ValueFormatter = valueFormatter
+            });
+            return this;
+        }
+    }
+
+    public struct PlayReportValueFormatterSpec
+    {
+        public required int Priority { get; init; }
+        public required string ReportKey { get; init; }
+        public required Func<object, string> ValueFormatter { get; init; }
+    }
+}

+ 1 - 1
src/Ryujinx.Horizon/HorizonStatic.cs

@@ -7,7 +7,7 @@ namespace Ryujinx.Horizon
 {
 {
     public static class HorizonStatic
     public static class HorizonStatic
     {
     {
-        internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted.Invoke(report);
+        internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted?.Invoke(report);
         
         
         public static event Action<MessagePackObject> PlayReportPrinted;
         public static event Action<MessagePackObject> PlayReportPrinted;
         
         

+ 24 - 34
src/Ryujinx/DiscordIntegrationModule.cs

@@ -5,6 +5,7 @@ using Ryujinx.Ava.Utilities;
 using Ryujinx.Ava.Utilities.AppLibrary;
 using Ryujinx.Ava.Utilities.AppLibrary;
 using Ryujinx.Ava.Utilities.Configuration;
 using Ryujinx.Ava.Utilities.Configuration;
 using Ryujinx.Common;
 using Ryujinx.Common;
+using Ryujinx.Common.Helper;
 using Ryujinx.Common.Logging;
 using Ryujinx.Common.Logging;
 using Ryujinx.HLE;
 using Ryujinx.HLE;
 using Ryujinx.HLE.Loaders.Processes;
 using Ryujinx.HLE.Loaders.Processes;
@@ -23,12 +24,12 @@ namespace Ryujinx.Ava
         public static Timestamps GuestAppStartedAt { get; set; }
         public static Timestamps GuestAppStartedAt { get; set; }
 
 
         private static string VersionString
         private static string VersionString
-            => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}"; 
+            => (ReleaseInformation.IsCanaryBuild ? "Canary " : string.Empty) + $"v{ReleaseInformation.Version}";
 
 
-        private static readonly string _description = 
-            ReleaseInformation.IsValid 
-                    ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}" 
-                    : "dev build";
+        private static readonly string _description =
+            ReleaseInformation.IsValid
+                ? $"{VersionString} {ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelSourceRepo}@{ReleaseInformation.BuildGitHash}"
+                : "dev build";
 
 
         private const string ApplicationId = "1293250299716173864";
         private const string ApplicationId = "1293250299716173864";
 
 
@@ -45,8 +46,7 @@ namespace Ryujinx.Ava
             {
             {
                 Assets = new Assets
                 Assets = new Assets
                 {
                 {
-                    LargeImageKey = "ryujinx",
-                    LargeImageText = TruncateToByteLength(_description)
+                    LargeImageKey = "ryujinx", LargeImageText = TruncateToByteLength(_description)
                 },
                 },
                 Details = "Main Menu",
                 Details = "Main Menu",
                 State = "Idling",
                 State = "Idling",
@@ -86,10 +86,10 @@ namespace Ryujinx.Ava
         {
         {
             if (titleId.TryGet(out string tid))
             if (titleId.TryGet(out string tid))
                 SwitchToPlayingState(
                 SwitchToPlayingState(
-                    ApplicationLibrary.LoadAndSaveMetaData(tid), 
+                    ApplicationLibrary.LoadAndSaveMetaData(tid),
                     Switch.Shared.Processes.ActiveApplication
                     Switch.Shared.Processes.ActiveApplication
                 );
                 );
-            else 
+            else
                 SwitchToMainState();
                 SwitchToMainState();
         }
         }
 
 
@@ -114,7 +114,7 @@ namespace Ryujinx.Ava
         {
         {
             _discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes));
             _discordClient?.SetPresence(_discordPresencePlaying ??= CreatePlayingState(appMeta, procRes));
         }
         }
-        
+
         private static void UpdatePlayingState()
         private static void UpdatePlayingState()
         {
         {
             _discordClient?.SetPresence(_discordPresencePlaying);
             _discordClient?.SetPresence(_discordPresencePlaying);
@@ -126,36 +126,26 @@ namespace Ryujinx.Ava
             _discordPresencePlaying = null;
             _discordPresencePlaying = null;
         }
         }
         
         
+        private static readonly PlayReportAnalyzer _playReportAnalyzer = new PlayReportAnalyzer()
+            .AddSpec( // Breath of the Wild
+                "01007ef00011e000",
+                gameSpec =>
+                    gameSpec.AddValueFormatter("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode")
+            );
+
         private static void HandlePlayReport(MessagePackObject playReport)
         private static void HandlePlayReport(MessagePackObject playReport)
         {
         {
             if (!TitleIDs.CurrentApplication.Value.HasValue) return;
             if (!TitleIDs.CurrentApplication.Value.HasValue) return;
             if (_discordPresencePlaying is null) return;
             if (_discordPresencePlaying is null) return;
-            if (!playReport.IsDictionary) return;
 
 
-            foreach ((string titleId, (string reportKey, Func<object, string> formatter)) in _playReportValues)
-            {
-                if (!TitleIDs.CurrentApplication.Value.Value.EqualsIgnoreCase(titleId))
-                    continue;
-                
-                if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
-                    return;
-
-                _discordPresencePlaying.Details = formatter(valuePackObject.ToObject());
-                UpdatePlayingState();
-                Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
-            }
-        }
+            Optional<string> details = _playReportAnalyzer.Run(TitleIDs.CurrentApplication.Value, playReport);
 
 
-        // title ID -> Play Report key & value formatter
-        private static readonly ReadOnlyDictionary<string, (string ReportKey, Func<object, string> Formatter)> 
-            _playReportValues = new(new Dictionary<string, (string ReportKey, Func<object, string> Formatter)>
-            {
-                {
-                    // Breath of the Wild Master Mode display
-                    "01007ef00011e000", 
-                    ("IsHardMode", val => val is 1 ? "Playing Master Mode" : "Playing Normal Mode")
-                }
-            });
+            if (!details.HasValue) return;
+            
+            _discordPresencePlaying.Details = details;
+            UpdatePlayingState();
+            Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
+        }
 
 
         private static string TruncateToByteLength(string input)
         private static string TruncateToByteLength(string input)
         {
         {