misc: chore: XMLDocs on PlayReportAnalyzer system.

- Change PlayReportValue to a basic class passed normally instead of a struct passed by reference
This commit is contained in:
Evan Husted 2025-02-03 18:54:38 -06:00
parent d8549f687b
commit f225b18c05
3 changed files with 199 additions and 77 deletions

View File

@ -135,7 +135,7 @@ namespace Ryujinx.Ava
if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) 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; if (!value.Handled) return;

View File

@ -1,10 +1,4 @@
using Gommon; using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Common.Helper;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities namespace Ryujinx.Ava.Utilities
{ {
@ -24,23 +18,23 @@ namespace Ryujinx.Ava.Utilities
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode) spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
) )
.AddSpec( .AddSpec(
"010075000ECBE000", "010075000ecbe000",
spec => spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode) spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
) )
.AddSpec( .AddSpec(
"010028600EBDA000", "010028600ebda000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
) )
.AddSpec( // Global & China IDs .AddSpec( // Global & China IDs
["0100152000022000", "010075100E8EC000"], ["0100152000022000", "010075100e8ec000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) 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; => 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 value.PackedValue.AsDouble() switch
{ {
> 800d => "Exploring the Sky Islands", > 800d => "Exploring the Sky Islands",
@ -48,16 +42,16 @@ namespace Ryujinx.Ava.Utilities
_ => "Roaming Hyrule" _ => "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"; => 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 普通模式"; => 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"; => 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 => value.BoxedValue switch
{ {
// Single Player // Single Player

View File

@ -3,127 +3,255 @@ using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
namespace Ryujinx.Ava.Utilities namespace Ryujinx.Ava.Utilities
{ {
/// <summary>
/// The entrypoint for the Play Report analysis system.
/// </summary>
public class PlayReportAnalyzer public class PlayReportAnalyzer
{ {
private readonly List<PlayReportGameSpec> _specs = []; 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) 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] })); _specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
return this; 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) 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)); _specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
return this; 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; 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) 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; 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) if (!playReport.IsDictionary)
return PlayReportFormattedValue.Unhandled; return FormattedValue.Unhandled;
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec)) 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)) if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
continue; continue;
PlayReportValue value = new() return formatSpec.ValueFormatter(new PlayReportValue
{ {
Application = appMeta, Application = appMeta, PackedValue = valuePackObject
PackedValue = valuePackObject });
};
return formatSpec.ValueFormatter(ref value);
} }
return PlayReportFormattedValue.Unhandled; 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;
/// <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 class PlayReportGameSpec
{ {
public required string[] TitleIds { get; init; } 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) public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
{ {
Analyses.Add(new PlayReportValueFormatterSpec SimpleValueFormatters.Add(new FormatterSpec
{ {
Priority = Analyses.Count, Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
ReportKey = reportKey,
ValueFormatter = valueFormatter
}); });
return this; 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, Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
ReportKey = reportKey,
ValueFormatter = valueFormatter
}); });
return this; 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; } public ApplicationMetadata Application { get; init; }
/// <summary>
/// The matched value from the Play Report.
/// </summary>
public MessagePackObject PackedValue { get; init; } 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 object BoxedValue => PackedValue.ToObject();
} }
public struct PlayReportFormattedValue /// <summary>
{ /// The delegate type that powers the entire analysis system (as it currently is).<br/>
public bool Handled { get; private init; } /// Takes in the result value from the Play Report, and outputs:
/// <br/>
public bool Reset { get; private init; } /// a formatted string,
/// <br/>
public string FormattedString { get; private init; } /// a signal that nothing was available to handle it,
/// <br/>
public static implicit operator PlayReportFormattedValue(string formattedValue) /// OR a signal to reset the value that the caller is using the <see cref="PlayReportAnalyzer"/> for.
=> new() { Handled = true, FormattedString = formattedValue }; /// </summary>
public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
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);
} }