diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs
index 4ea413ea1..f1ef1448f 100644
--- a/src/Ryujinx/DiscordIntegrationModule.cs
+++ b/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;
diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs
index 9049dec1b..c8649bcc0 100644
--- a/src/Ryujinx/Utilities/PlayReport.cs
+++ b/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
diff --git a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs b/src/Ryujinx/Utilities/PlayReportAnalyzer.cs
index 3760204a7..e2a520907 100644
--- a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs
+++ b/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
{
+ ///
+ /// The entrypoint for the Play Report analysis system.
+ ///
public class PlayReportAnalyzer
{
private readonly List _specs = [];
+ ///
+ /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
+ ///
+ /// The ID of the game to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
public PlayReportAnalyzer AddSpec(string titleId, Func 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;
}
-
+
+ ///
+ /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
+ ///
+ /// The ID of the game to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
public PlayReportAnalyzer AddSpec(string titleId, Action 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 titleIds, Func transform)
+
+ ///
+ /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
+ ///
+ /// The IDs of the games to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
+ public PlayReportAnalyzer AddSpec(IEnumerable titleIds,
+ Func 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;
}
-
+
+ ///
+ /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
+ ///
+ /// The IDs of the games to listen to Play Reports in.
+ /// The configuration function for the analysis spec.
+ /// The current , for chaining convenience.
public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Action 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)
+
+ ///
+ /// Runs the configured for the specified game title ID.
+ ///
+ /// The game currently running.
+ /// The Application metadata information, including localized game name and play time information.
+ /// The Play Report received from HLE.
+ /// A struct representing a possible formatted value.
+ 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 PlayReportFormattedValue.Unhandled;
+
+ return FormattedValue.Unhandled;
+ }
+
+ ///
+ /// A potential formatted value returned by a .
+ ///
+ public struct FormattedValue
+ {
+ ///
+ /// Was any handler able to match anything in the Play Report?
+ ///
+ public bool Handled { get; private init; }
+
+ ///
+ /// Did the handler request the caller of the to reset the existing value?
+ ///
+ public bool Reset { get; private init; }
+
+ ///
+ /// The formatted value, only present if is true, and is false.
+ ///
+ public string FormattedString { get; private init; }
+
+ ///
+ /// The intended path of execution for having a string to return: simply return the string.
+ /// This implicit conversion will make the struct for you.
+ ///
+ /// If the input is null, is returned.
+ ///
+ /// The formatted string value.
+ /// The automatically constructed struct.
+ public static implicit operator FormattedValue(string formattedValue)
+ => formattedValue is not null
+ ? new FormattedValue { Handled = true, FormattedString = formattedValue }
+ : Unhandled;
+
+ ///
+ /// Return this to tell the caller there is no value to return.
+ ///
+ public static FormattedValue Unhandled => default;
+
+ ///
+ /// Return this to suggest the caller reset the value it's using the for.
+ ///
+ public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
+
+ ///
+ /// A delegate singleton you can use to always return in a .
+ ///
+ public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
}
-
}
+ ///
+ /// A mapping of title IDs to value formatter specs.
+ ///
+ /// Generally speaking, use the .AddSpec(...) methods instead of creating this class yourself.
+ ///
public class PlayReportGameSpec
{
public required string[] TitleIds { get; init; }
- public List Analyses { get; } = [];
+ public List SimpleValueFormatters { get; } = [];
+ ///
+ /// Add a value formatter to the current
+ /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The key name to match.
+ /// The function which can return a potential formatted value.
+ /// The current , for chaining convenience.
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)
+
+ ///
+ /// Add a value formatter at a specific priority to the current
+ /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
+ ///
+ /// The resolution priority of this value formatter. Higher resolves sooner.
+ /// The key name to match.
+ /// The function which can return a potential formatted value.
+ /// The current , for chaining convenience.
+ 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;
}
+
+ ///
+ /// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
+ ///
+ public struct FormatterSpec
+ {
+ public required int Priority { get; init; }
+ public required string ReportKey { get; init; }
+ public PlayReportValueFormatter ValueFormatter { get; init; }
+ }
}
- public readonly struct PlayReportValue
+ ///
+ /// The input data to a ,
+ /// containing the currently running application's ,
+ /// and the matched from the Play Report.
+ ///
+ public class PlayReportValue
{
+ ///
+ /// The currently running application's .
+ ///
public ApplicationMetadata Application { get; init; }
-
+
+ ///
+ /// The matched value from the Play Report.
+ ///
public MessagePackObject PackedValue { get; init; }
+ ///
+ /// Access the as its underlying .NET type.
+ ///
+ /// Does not seem to work well with comparing numeric types,
+ /// so use and the AsX (where X is a numerical type name i.e. Int32) methods for that.
+ ///
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);
+ ///
+ /// The delegate type that powers the entire analysis system (as it currently is).
+ /// Takes in the result value from the Play Report, and outputs:
+ ///
+ /// a formatted string,
+ ///
+ /// a signal that nothing was available to handle it,
+ ///
+ /// OR a signal to reset the value that the caller is using the for.
+ ///
+ public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
}