Просмотр исходного кода

misc: chore: XMLDocs on PlayReportAnalyzer system.

- Change PlayReportValue to a basic class passed normally instead of a struct passed by reference
Evan Husted 1 год назад
Родитель
Сommit
f225b18c05

+ 1 - 1
src/Ryujinx/DiscordIntegrationModule.cs

@@ -135,7 +135,7 @@ namespace Ryujinx.Ava
             if (!TitleIDs.CurrentApplication.Value.HasValue) return;
             if (_discordPresencePlaying is null) return;
 
-            PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
+            PlayReportAnalyzer.FormattedValue value = PlayReport.Analyzer.FormatPlayReportValue(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
 
             if (!value.Handled) return;
 

+ 10 - 16
src/Ryujinx/Utilities/PlayReport.cs

@@ -1,10 +1,4 @@
-using Gommon;
-using MsgPack;
-using Ryujinx.Ava.Utilities.AppLibrary;
-using Ryujinx.Common.Helper;
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
 
 namespace Ryujinx.Ava.Utilities
 {
@@ -24,23 +18,23 @@ namespace Ryujinx.Ava.Utilities
                     spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
             )
             .AddSpec(
-                "010075000ECBE000",
+                "010075000ecbe000",
                 spec =>
                     spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
             )
             .AddSpec(
-                "010028600EBDA000",
+                "010028600ebda000",
                 spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
             )
             .AddSpec( // Global & China IDs
-                ["0100152000022000", "010075100E8EC000"],
+                ["0100152000022000", "010075100e8ec000"],
                 spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
             );
 
-        private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value)
+        private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
             => value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
 
-        private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(ref PlayReportValue value) =>
+        private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) =>
             value.PackedValue.AsDouble() switch
             {
                 > 800d => "Exploring the Sky Islands",
@@ -48,16 +42,16 @@ namespace Ryujinx.Ava.Utilities
                 _ => "Roaming Hyrule"
             };
 
-        private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value)
+        private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value)
             => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
 
-        private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value)
+        private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
             => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
 
-        private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value)
+        private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
             => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
         
-        private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value) 
+        private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value) 
             => value.BoxedValue switch
             {
                 // Single Player

+ 187 - 59
src/Ryujinx/Utilities/PlayReportAnalyzer.cs

@@ -3,127 +3,255 @@ using MsgPack;
 using Ryujinx.Ava.Utilities.AppLibrary;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 
 namespace Ryujinx.Ava.Utilities
 {
+    /// <summary>
+    /// The entrypoint for the Play Report analysis system.
+    /// </summary>
     public class PlayReportAnalyzer
     {
         private readonly List<PlayReportGameSpec> _specs = [];
 
+        /// <summary>
+        /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
+        /// </summary>
+        /// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
+        /// <param name="transform">The configuration function for the analysis spec.</param>
+        /// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
         public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
         {
+            Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
+                $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
+
             _specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
             return this;
         }
-        
+
+        /// <summary>
+        /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
+        /// </summary>
+        /// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
+        /// <param name="transform">The configuration function for the analysis spec.</param>
+        /// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
         public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
         {
+            Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
+                $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
+
             _specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
             return this;
         }
-        
-        public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
+
+        /// <summary>
+        /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
+        /// </summary>
+        /// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
+        /// <param name="transform">The configuration function for the analysis spec.</param>
+        /// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
+        public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds,
+            Func<PlayReportGameSpec, PlayReportGameSpec> transform)
         {
-            _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] }));
+            string[] tids = titleIds.ToArray();
+            Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
+                $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
+
+            _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] }));
             return this;
         }
-        
+
+        /// <summary>
+        /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
+        /// </summary>
+        /// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
+        /// <param name="transform">The configuration function for the analysis spec.</param>
+        /// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
         public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
         {
-            _specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform));
+            string[] tids = titleIds.ToArray();
+            Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
+                $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
+
+            _specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform));
             return this;
         }
 
-        public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport)
+        
+        /// <summary>
+        /// Runs the configured <see cref="PlayReportGameSpec.FormatterSpec"/> for the specified game title ID.
+        /// </summary>
+        /// <param name="runningGameId">The game currently running.</param>
+        /// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
+        /// <param name="playReport">The Play Report received from HLE.</param>
+        /// <returns>A struct representing a possible formatted value.</returns>
+        public FormattedValue FormatPlayReportValue(
+            string runningGameId,
+            ApplicationMetadata appMeta,
+            MessagePackObject playReport
+        )
         {
-            if (!playReport.IsDictionary) 
-                return PlayReportFormattedValue.Unhandled;
+            if (!playReport.IsDictionary)
+                return FormattedValue.Unhandled;
 
             if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
-                return PlayReportFormattedValue.Unhandled;
+                return FormattedValue.Unhandled;
 
-            foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority))
+            foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
             {
                 if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
                     continue;
 
-                PlayReportValue value = new()
+                return formatSpec.ValueFormatter(new PlayReportValue
                 {
-                    Application = appMeta, 
-                    PackedValue = valuePackObject
-                };
-
-                return formatSpec.ValueFormatter(ref value);
+                    Application = appMeta, PackedValue = valuePackObject
+                });
             }
+
+            return FormattedValue.Unhandled;
+        }
+
+        /// <summary>
+        /// A potential formatted value returned by a <see cref="PlayReportValueFormatter"/>.
+        /// </summary>
+        public struct FormattedValue
+        {
+            /// <summary>
+            /// Was any handler able to match anything in the Play Report?
+            /// </summary>
+            public bool Handled { get; private init; }
+
+            /// <summary>
+            /// Did the handler request the caller of the <see cref="PlayReportAnalyzer"/> to reset the existing value?
+            /// </summary>
+            public bool Reset { get; private init; }
+
+            /// <summary>
+            /// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
+            /// </summary>
+            public string FormattedString { get; private init; }
+
+            /// <summary>
+            /// The intended path of execution for having a string to return: simply return the string.
+            /// This implicit conversion will make the struct for you.<br/><br/>
+            ///
+            /// If the input is null, <see cref="Unhandled"/> is returned.
+            /// </summary>
+            /// <param name="formattedValue">The formatted string value.</param>
+            /// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
+            public static implicit operator FormattedValue(string formattedValue)
+                => formattedValue is not null 
+                    ? new FormattedValue { Handled = true, FormattedString = formattedValue }
+                    : Unhandled;
+
+            /// <summary>
+            /// Return this to tell the caller there is no value to return.
+            /// </summary>
+            public static FormattedValue Unhandled => default;
             
-            return PlayReportFormattedValue.Unhandled;
+            /// <summary>
+            /// Return this to suggest the caller reset the value it's using the <see cref="PlayReportAnalyzer"/> for.
+            /// </summary>
+            public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
+
+            /// <summary>
+            /// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="PlayReportValueFormatter"/>.
+            /// </summary>
+            public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
         }
-        
     }
 
+    /// <summary>
+    /// A mapping of title IDs to value formatter specs.
+    ///
+    /// <remarks>Generally speaking, use the <see cref="PlayReportAnalyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
+    /// </summary>
     public class PlayReportGameSpec
     {
         public required string[] TitleIds { get; init; }
-        public List<PlayReportValueFormatterSpec> Analyses { get; } = [];
+        public List<FormatterSpec> SimpleValueFormatters { get; } = [];
 
+        /// <summary>
+        /// Add a value formatter to the current <see cref="PlayReportGameSpec"/>
+        /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
+        /// </summary>
+        /// <param name="reportKey">The key name to match.</param>
+        /// <param name="valueFormatter">The function which can return a potential formatted value.</param>
+        /// <returns>The current <see cref="PlayReportGameSpec"/>, for chaining convenience.</returns>
         public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
         {
-            Analyses.Add(new PlayReportValueFormatterSpec
+            SimpleValueFormatters.Add(new FormatterSpec
             {
-                Priority = Analyses.Count,
-                ReportKey = reportKey, 
-                ValueFormatter = valueFormatter
+                Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
             });
             return this;
         }
-        
-        public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter valueFormatter)
+
+        /// <summary>
+        /// Add a value formatter at a specific priority to the current <see cref="PlayReportGameSpec"/>
+        /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
+        /// </summary>
+        /// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
+        /// <param name="reportKey">The key name to match.</param>
+        /// <param name="valueFormatter">The function which can return a potential formatted value.</param>
+        /// <returns>The current <see cref="PlayReportGameSpec"/>, for chaining convenience.</returns>
+        public PlayReportGameSpec AddValueFormatter(int priority, string reportKey,
+            PlayReportValueFormatter valueFormatter)
         {
-            Analyses.Add(new PlayReportValueFormatterSpec
+            SimpleValueFormatters.Add(new FormatterSpec
             {
-                Priority = priority,
-                ReportKey = reportKey, 
-                ValueFormatter = valueFormatter
+                Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
             });
             return this;
         }
+
+        /// <summary>
+        /// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
+        /// </summary>
+        public struct FormatterSpec
+        {
+            public required int Priority { get; init; }
+            public required string ReportKey { get; init; }
+            public PlayReportValueFormatter ValueFormatter { get; init; }
+        }
     }
 
-    public readonly struct PlayReportValue
+    /// <summary>
+    /// The input data to a <see cref="PlayReportValueFormatter"/>,
+    /// containing the currently running application's <see cref="ApplicationMetadata"/>,
+    /// and the matched <see cref="MessagePackObject"/> from the Play Report.
+    /// </summary>
+    public class PlayReportValue
     {
+        /// <summary>
+        /// The currently running application's <see cref="ApplicationMetadata"/>.
+        /// </summary>
         public ApplicationMetadata Application { get; init; }
-        
+
+        /// <summary>
+        /// The matched value from the Play Report.
+        /// </summary>
         public MessagePackObject PackedValue { get; init; }
 
+        /// <summary>
+        /// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
+        /// 
+        /// Does not seem to work well with comparing numeric types,
+        /// so use <see cref="PackedValue"/> and the AsX (where X is a numerical type name i.e. Int32) methods for that.
+        /// </summary>
         public object BoxedValue => PackedValue.ToObject();
     }
 
-    public struct PlayReportFormattedValue
-    {
-        public bool Handled { get; private init; }
-        
-        public bool Reset { get; private init; }
-        
-        public string FormattedString { get; private init; }
-
-        public static implicit operator PlayReportFormattedValue(string formattedValue)
-            => new() { Handled = true, FormattedString = formattedValue };
-
-        public static PlayReportFormattedValue Unhandled => default;
-        public static PlayReportFormattedValue ForceReset => new() { Handled = true, Reset = true };
-
-        public static PlayReportValueFormatter AlwaysResets = AlwaysResetsImpl;
-        
-        private static PlayReportFormattedValue AlwaysResetsImpl(ref PlayReportValue _) => ForceReset;
-    }
-
-    public struct PlayReportValueFormatterSpec
-    {
-        public required int Priority { get; init; }
-        public required string ReportKey { get; init; }
-        public PlayReportValueFormatter ValueFormatter { get; init; }
-    }
-
-    public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value);
+    /// <summary>
+    /// The delegate type that powers the entire analysis system (as it currently is).<br/>
+    /// Takes in the result value from the Play Report, and outputs:
+    /// <br/>
+    /// a formatted string,
+    /// <br/>
+    /// a signal that nothing was available to handle it,
+    /// <br/>
+    /// OR a signal to reset the value that the caller is using the <see cref="PlayReportAnalyzer"/> for. 
+    /// </summary>
+    public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
 }