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.PlayReport
{
///
/// The entrypoint for the Play Report analysis system.
///
public class Analyzer
{
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 Analyzer 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(GameSpec)}.");
_specs.Add(transform(new GameSpec { 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 Analyzer 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(GameSpec)}.");
_specs.Add(new GameSpec { 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 Analyzer 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(GameSpec)}.");
_specs.Add(transform(new GameSpec { 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 Analyzer 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(GameSpec)}.");
_specs.Add(new GameSpec { 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 GameSpec spec))
return FormattedValue.Unhandled;
foreach (GameSpec.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
});
}
foreach (GameSpec.MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority))
{
List 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
.Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
.ToArray());
}
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 ValueFormatter 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 ValueFormatter 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 GameSpec
{
public required string[] TitleIds { get; init; }
public List SimpleValueFormatters { get; } = [];
public List MultiValueFormatters { 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 GameSpec AddValueFormatter(string reportKey, ValueFormatter 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 GameSpec AddValueFormatter(int priority, string reportKey,
ValueFormatter valueFormatter)
{
SimpleValueFormatters.Add(new FormatterSpec
{
Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
});
return this;
}
///
/// Add a multi-value formatter to the current
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
///
/// The key names to match.
/// The function which can format the values.
/// The current , for chaining convenience.
public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
{
MultiValueFormatters.Add(new MultiFormatterSpec
{
Priority = SimpleValueFormatters.Count, ReportKeys = reportKeys, ValueFormatter = valueFormatter
});
return this;
}
///
/// Add a multi-value formatter at a specific priority to the current
/// matching a specific set of keys 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 names to match.
/// The function which can format the values.
/// The current , for chaining convenience.
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
MultiValueFormatter valueFormatter)
{
MultiValueFormatters.Add(new MultiFormatterSpec
{
Priority = priority, ReportKeys = reportKeys, 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 ValueFormatter ValueFormatter { get; init; }
}
///
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
///
public struct MultiFormatterSpec
{
public required int Priority { get; init; }
public required string[] ReportKeys { get; init; }
public MultiValueFormatter ValueFormatter { get; init; }
}
}
///
/// The input data to a ,
/// containing the currently running application's ,
/// and the matched from the Play Report.
///
public class Value
{
///
/// 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 XValue properties 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 single value formatters.
/// 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 Analyzer.FormattedValue ValueFormatter(Value value);
///
/// The delegate type that powers multiple value formatters.
/// 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 Analyzer.FormattedValue MultiValueFormatter(Value[] value);
}