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);
}