using Gommon; 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; } /// /// 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) { 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) { 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; } /// /// 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 Format( string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport ) { if (!playReport.IsDictionary) return FormattedValue.Unhandled; if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec)) return FormattedValue.Unhandled; foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) { if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) continue; return formatSpec.ValueFormatter(new PlayReportValue { Application = appMeta, PackedValue = valuePackObject }); } return FormattedValue.Unhandled; } /// /// A potential formatted value returned by a . /// public readonly 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 delegate factory you can use to always return the specified /// in a . /// /// The string to always return for this delegate instance. public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue; } } /// /// 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 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) { SimpleValueFormatters.Add(new FormatterSpec { Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter }); return this; } /// /// 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) { SimpleValueFormatters.Add(new FormatterSpec { 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; } } } /// /// 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(); #region AsX accessors 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 BinaryValue => PackedValue.AsBinary(); #endregion } /// /// 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); }