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

PlayReport: Add Sparse Multi Value formatters

Evan Husted 1 год назад
Родитель
Сommit
2c8edaf89e

+ 6 - 4
src/Ryujinx/DiscordIntegrationModule.cs

@@ -126,14 +126,16 @@ namespace Ryujinx.Ava
             if (!TitleIDs.CurrentApplication.Value.HasValue) return;
             if (_discordPresencePlaying is null) return;
 
-            Analyzer.FormattedValue formattedValue =
+            FormattedValue formattedValue =
                 PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
 
             if (!formattedValue.Handled) return;
 
-            _discordPresencePlaying.Details = formattedValue.Reset 
-                ? $"Playing {_currentApp.Title}" 
-                : formattedValue.FormattedString;
+            _discordPresencePlaying.Details = TruncateToByteLength(
+                formattedValue.Reset
+                    ? $"Playing {_currentApp.Title}"
+                    : formattedValue.FormattedString
+            );
 
             if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
                 return; //don't trigger an update if the set presence Details are identical to current

+ 21 - 234
src/Ryujinx/Utilities/PlayReport/Analyzer.cs

@@ -78,7 +78,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
             return this;
         }
 
-        
+
         /// <summary>
         /// Runs the configured <see cref="GameSpec.FormatterSpec"/> for the specified game title ID.
         /// </summary>
@@ -98,261 +98,48 @@ namespace Ryujinx.Ava.Utilities.PlayReport
             if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
                 return FormattedValue.Unhandled;
 
-            foreach (GameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
+            foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
             {
                 if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
                     continue;
 
-                return formatSpec.ValueFormatter(new Value
-                {
-                    Application = appMeta, PackedValue = valuePackObject
-                });
+                return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject });
             }
-            
-            foreach (GameSpec.MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
+
+            foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
             {
                 List<MessagePackObject> packedObjects = [];
                 foreach (var reportKey in formatSpec.ReportKeys)
                 {
                     if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
                         continue;
-                    
+
                     packedObjects.Add(valuePackObject);
                 }
-                
+
                 if (packedObjects.Count != formatSpec.ReportKeys.Length)
                     return FormattedValue.Unhandled;
-                
-                return formatSpec.ValueFormatter(packedObjects
+
+                return formatSpec.Formatter(packedObjects
                     .Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
                     .ToArray());
             }
 
-            return FormattedValue.Unhandled;
-        }
-
-        /// <summary>
-        /// A potential formatted value returned by a <see cref="ValueFormatter"/>.
-        /// </summary>
-        public readonly 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="Analyzer"/> 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;
-            
-            /// <summary>
-            /// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> 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="ValueFormatter"/>.
-            /// </summary>
-            public static readonly ValueFormatter AlwaysResets = _ => ForceReset;
-
-            /// <summary>
-            /// A delegate factory you can use to always return the specified
-            /// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
-            /// </summary>
-            /// <param name="formattedValue">The string to always return for this delegate instance.</param>
-            public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
-        }
-    }
-
-    /// <summary>
-    /// A mapping of title IDs to value formatter specs.
-    ///
-    /// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
-    /// </summary>
-    public class GameSpec
-    {
-        public required string[] TitleIds { get; init; }
-        public List<FormatterSpec> SimpleValueFormatters { get; } = [];
-        public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
-
-        /// <summary>
-        /// Add a value formatter to the current <see cref="GameSpec"/>
-        /// 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="GameSpec"/>, for chaining convenience.</returns>
-        public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
-        {
-            SimpleValueFormatters.Add(new FormatterSpec
-            {
-                Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
-            });
-            return this;
-        }
-
-        /// <summary>
-        /// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
-        /// 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="GameSpec"/>, for chaining convenience.</returns>
-        public GameSpec AddValueFormatter(int priority, string reportKey,
-            ValueFormatter valueFormatter)
-        {
-            SimpleValueFormatters.Add(new FormatterSpec
-            {
-                Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
-            });
-            return this;
-        }
-        
-        /// <summary>
-        /// Add a multi-value formatter to the current <see cref="GameSpec"/>
-        /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
-        /// </summary>
-        /// <param name="reportKeys">The key names to match.</param>
-        /// <param name="valueFormatter">The function which can format the values.</param>
-        /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
-        public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
-        {
-            MultiValueFormatters.Add(new MultiFormatterSpec
-            {
-                Priority = SimpleValueFormatters.Count, ReportKeys = reportKeys, ValueFormatter = valueFormatter
-            });
-            return this;
-        }
-        
-        /// <summary>
-        /// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
-        /// matching a specific set of keys 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="reportKeys">The key names to match.</param>
-        /// <param name="valueFormatter">The function which can format the values.</param>
-        /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
-        public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
-            MultiValueFormatter valueFormatter)
-        {
-            MultiValueFormatters.Add(new MultiFormatterSpec
+            foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority))
             {
-                Priority = priority, ReportKeys = reportKeys, 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 ValueFormatter ValueFormatter { get; init; }
-        }
-        
-        /// <summary>
-        /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
-        /// </summary>
-        public struct MultiFormatterSpec
-        {
-            public required int Priority { get; init; }
-            public required string[] ReportKeys { get; init; }
-            public MultiValueFormatter ValueFormatter { get; init; }
-        }
-    }
-
-    /// <summary>
-    /// The input data to a <see cref="ValueFormatter"/>,
-    /// containing the currently running application's <see cref="ApplicationMetadata"/>,
-    /// and the matched <see cref="MessagePackObject"/> from the Play Report.
-    /// </summary>
-    public class Value
-    {
-        /// <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 XValue properties for that.
-        /// </summary>
-        public object BoxedValue => PackedValue.ToObject();
+                Dictionary<string, Value> packedObjects = [];
+                foreach (var reportKey in formatSpec.ReportKeys)
+                {
+                    if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
+                        continue;
 
-        #region AsX accessors
+                    packedObjects.Add(reportKey, new Value { Application = appMeta, PackedValue = valuePackObject });
+                }
 
-        public bool BooleanValue => PackedValue.AsBoolean();
-        public byte ByteValye => PackedValue.AsByte();
-        public sbyte SByteValye => PackedValue.AsSByte();
-        public short ShortValye => PackedValue.AsInt16();
-        public ushort UShortValye => PackedValue.AsUInt16();
-        public int IntValye => PackedValue.AsInt32();
-        public uint UIntValye => PackedValue.AsUInt32();
-        public long LongValye => PackedValue.AsInt64();
-        public ulong ULongValye => PackedValue.AsUInt64();
-        public float FloatValue => PackedValue.AsSingle();
-        public double DoubleValue => PackedValue.AsDouble();
-        public string StringValue => PackedValue.AsString();
-        public Span<byte> BinaryValue => PackedValue.AsBinary();
+                return formatSpec.Formatter(packedObjects);
+            }
 
-        #endregion
+            return FormattedValue.Unhandled;
+        }
     }
-
-    /// <summary>
-    /// The delegate type that powers single value formatters.<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="Analyzer"/> for. 
-    /// </summary>
-    public delegate Analyzer.FormattedValue ValueFormatter(Value value);
-    
-    /// <summary>
-    /// The delegate type that powers multiple value formatters.<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="Analyzer"/> for. 
-    /// </summary>
-    public delegate Analyzer.FormattedValue MultiValueFormatter(Value[] value);
 }

+ 42 - 0
src/Ryujinx/Utilities/PlayReport/Delegates.cs

@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+    /// <summary>
+    /// The delegate type that powers single value formatters.<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="Analyzer"/> for. 
+    /// </summary>
+    public delegate FormattedValue ValueFormatter(Value value);
+
+    /// <summary>
+    /// The delegate type that powers multiple value formatters.<br/>
+    /// Takes in the result values 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="Analyzer"/> for. 
+    /// </summary>
+    public delegate FormattedValue MultiValueFormatter(Value[] value);
+
+    /// <summary>
+    /// The delegate type that powers multiple value formatters.
+    /// The dictionary passed to this delegate is sparsely populated;
+    /// that is, not every key specified in the Play Report needs to match for this to be used.<br/>
+    /// Takes in the result values 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="Analyzer"/> for. 
+    /// </summary>
+    public delegate FormattedValue SparseMultiValueFormatter(Dictionary<string, Value> values);
+}

+ 2 - 4
src/Ryujinx/Utilities/PlayReport/PlayReports.cs

@@ -1,6 +1,4 @@
-using static Ryujinx.Ava.Utilities.PlayReport.Analyzer;
-
-namespace Ryujinx.Ava.Utilities.PlayReport
+namespace Ryujinx.Ava.Utilities.PlayReport
 {
     public static class PlayReports
     {
@@ -10,7 +8,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
                 spec => spec
                     .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
                     // reset to normal status when switching between normal & master mode in title screen
-                    .AddValueFormatter("AoCVer", FormattedValue.AlwaysResets)
+                    .AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets)
             )
             .AddSpec(
                 "0100f2c0115b6000",

+ 140 - 0
src/Ryujinx/Utilities/PlayReport/Specs.cs

@@ -0,0 +1,140 @@
+using FluentAvalonia.Core;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+    /// <summary>
+    /// A mapping of title IDs to value formatter specs.
+    ///
+    /// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
+    /// </summary>
+    public class GameSpec
+    {
+        public required string[] TitleIds { get; init; }
+        public List<FormatterSpec> SimpleValueFormatters { get; } = [];
+        public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
+        public List<SparseMultiFormatterSpec> SparseMultiValueFormatters { get; } = [];
+
+
+        /// <summary>
+        /// Add a value formatter to the current <see cref="GameSpec"/>
+        /// 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="GameSpec"/>, for chaining convenience.</returns>
+        public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
+            => AddValueFormatter(SimpleValueFormatters.Count, reportKey, valueFormatter);
+
+        /// <summary>
+        /// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
+        /// 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="GameSpec"/>, for chaining convenience.</returns>
+        public GameSpec AddValueFormatter(int priority, string reportKey,
+            ValueFormatter valueFormatter)
+        {
+            SimpleValueFormatters.Add(new FormatterSpec
+            {
+                Priority = priority, ReportKey = reportKey, Formatter = valueFormatter
+            });
+            return this;
+        }
+
+        /// <summary>
+        /// Add a multi-value formatter to the current <see cref="GameSpec"/>
+        /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+        /// </summary>
+        /// <param name="reportKeys">The key names to match.</param>
+        /// <param name="valueFormatter">The function which can format the values.</param>
+        /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
+        public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
+            => AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter);
+
+        /// <summary>
+        /// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
+        /// matching a specific set of keys 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="reportKeys">The key names to match.</param>
+        /// <param name="valueFormatter">The function which can format the values.</param>
+        /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
+        public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
+            MultiValueFormatter valueFormatter)
+        {
+            MultiValueFormatters.Add(new MultiFormatterSpec
+            {
+                Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
+            });
+            return this;
+        }
+
+        /// <summary>
+        /// Add a multi-value formatter to the current <see cref="GameSpec"/>
+        /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+        /// <br/><br/>
+        /// The 'Sparse' multi-value formatters do not require every key to be present.
+        /// If you need this requirement, use <see cref="AddMultiValueFormatter(string[], Ryujinx.Ava.Utilities.PlayReport.MultiValueFormatter)"/>.
+        /// </summary>
+        /// <param name="reportKeys">The key names to match.</param>
+        /// <param name="valueFormatter">The function which can format the values.</param>
+        /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
+        public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter)
+            => AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter);
+
+        /// <summary>
+        /// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
+        /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
+        /// <br/><br/>
+        /// The 'Sparse' multi-value formatters do not require every key to be present.
+        /// If you need this requirement, use <see cref="AddMultiValueFormatter(int, string[], Ryujinx.Ava.Utilities.PlayReport.MultiValueFormatter)"/>.
+        /// </summary>
+        /// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
+        /// <param name="reportKeys">The key names to match.</param>
+        /// <param name="valueFormatter">The function which can format the values.</param>
+        /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
+        public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys,
+            SparseMultiValueFormatter valueFormatter)
+        {
+            SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec
+            {
+                Priority = priority, ReportKeys = reportKeys, Formatter = 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 ValueFormatter Formatter { get; init; }
+    }
+
+    /// <summary>
+    /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
+    /// </summary>
+    public struct MultiFormatterSpec
+    {
+        public required int Priority { get; init; }
+        public required string[] ReportKeys { get; init; }
+        public MultiValueFormatter Formatter { get; init; }
+    }
+
+    /// <summary>
+    /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their sparsely populated potential values.
+    /// </summary>
+    public struct SparseMultiFormatterSpec
+    {
+        public required int Priority { get; init; }
+        public required string[] ReportKeys { get; init; }
+        public SparseMultiValueFormatter Formatter { get; init; }
+    }
+}

+ 130 - 0
src/Ryujinx/Utilities/PlayReport/Value.cs

@@ -0,0 +1,130 @@
+using MsgPack;
+using Ryujinx.Ava.Utilities.AppLibrary;
+using System;
+
+namespace Ryujinx.Ava.Utilities.PlayReport
+{
+    /// <summary>
+    /// The input data to a <see cref="ValueFormatter"/>,
+    /// containing the currently running application's <see cref="ApplicationMetadata"/>,
+    /// and the matched <see cref="MessagePackObject"/> from the Play Report.
+    /// </summary>
+    public class Value
+    {
+        /// <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 XValue properties for that.
+        /// </summary>
+        public object BoxedValue => PackedValue.ToObject();
+
+        public override string ToString()
+        {
+            object boxed = BoxedValue;
+            return boxed == null
+                ? "null"
+                : boxed.ToString();
+        }
+
+        #region AsX accessors
+
+        public bool BooleanValue => PackedValue.AsBoolean();
+        public byte ByteValue => PackedValue.AsByte();
+        public sbyte SByteValue => PackedValue.AsSByte();
+        public short ShortValue => PackedValue.AsInt16();
+        public ushort UShortValue => PackedValue.AsUInt16();
+        public int IntValue => PackedValue.AsInt32();
+        public uint UIntValue => PackedValue.AsUInt32();
+        public long LongValue => PackedValue.AsInt64();
+        public ulong ULongValue => PackedValue.AsUInt64();
+        public float FloatValue => PackedValue.AsSingle();
+        public double DoubleValue => PackedValue.AsDouble();
+        public string StringValue => PackedValue.AsString();
+        public Span<byte> BinaryValue => PackedValue.AsBinary();
+
+        #endregion
+    }
+
+    /// <summary>
+    /// A potential formatted value returned by a <see cref="ValueFormatter"/>.
+    /// </summary>
+    public readonly 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="Analyzer"/> 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;
+
+        public override string ToString()
+        {
+            if (!Handled)
+                return "<Unhandled>";
+
+            if (Reset)
+                return "<Reset>";
+
+            return FormattedString;
+        }
+
+        /// <summary>
+        /// Return this to tell the caller there is no value to return.
+        /// </summary>
+        public static FormattedValue Unhandled => default;
+
+        /// <summary>
+        /// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> 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="ValueFormatter"/>.
+        /// </summary>
+        public static readonly ValueFormatter SingleAlwaysResets = _ => ForceReset;
+
+        /// <summary>
+        /// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="MultiValueFormatter"/>.
+        /// </summary>
+        public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset;
+
+        /// <summary>
+        /// A delegate factory you can use to always return the specified
+        /// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
+        /// </summary>
+        /// <param name="formattedValue">The string to always return for this delegate instance.</param>
+        public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
+    }
+}