From 2c8edaf89e29b33c5f3021269b58345f47cec24f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Fri, 7 Feb 2025 15:43:50 -0600 Subject: [PATCH] PlayReport: Add Sparse Multi Value formatters --- src/Ryujinx/DiscordIntegrationModule.cs | 10 +- src/Ryujinx/Utilities/PlayReport/Analyzer.cs | 259 ++---------------- src/Ryujinx/Utilities/PlayReport/Delegates.cs | 42 +++ .../Utilities/PlayReport/PlayReports.cs | 6 +- src/Ryujinx/Utilities/PlayReport/Specs.cs | 140 ++++++++++ src/Ryujinx/Utilities/PlayReport/Value.cs | 130 +++++++++ 6 files changed, 343 insertions(+), 244 deletions(-) create mode 100644 src/Ryujinx/Utilities/PlayReport/Delegates.cs create mode 100644 src/Ryujinx/Utilities/PlayReport/Specs.cs create mode 100644 src/Ryujinx/Utilities/PlayReport/Value.cs diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index d95bb80dd..abdd9fed1 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -126,14 +126,16 @@ namespace Ryujinx.Ava if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - Analyzer.FormattedValue formattedValue = + FormattedValue formattedValue = PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport); if (!formattedValue.Handled) return; - _discordPresencePlaying.Details = formattedValue.Reset - ? $"Playing {_currentApp.Title}" - : formattedValue.FormattedString; + _discordPresencePlaying.Details = TruncateToByteLength( + formattedValue.Reset + ? $"Playing {_currentApp.Title}" + : formattedValue.FormattedString + ); if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details)) return; //don't trigger an update if the set presence Details are identical to current diff --git a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs index 84bdbf085..390e06d28 100644 --- a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs +++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs @@ -78,7 +78,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport return this; } - + /// /// Runs the configured for the specified game title ID. /// @@ -98,261 +98,48 @@ namespace Ryujinx.Ava.Utilities.PlayReport 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)) + foreach (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 - }); + return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject }); } - - foreach (GameSpec.MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority)) + + foreach (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 + + return formatSpec.Formatter(packedObjects .Select(packObject => new Value { Application = appMeta, PackedValue = packObject }) .ToArray()); } + foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority)) + { + Dictionary packedObjects = []; + foreach (var reportKey in formatSpec.ReportKeys) + { + if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + continue; + + packedObjects.Add(reportKey, new Value { Application = appMeta, PackedValue = valuePackObject }); + } + + return formatSpec.Formatter(packedObjects); + } + 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); } diff --git a/src/Ryujinx/Utilities/PlayReport/Delegates.cs b/src/Ryujinx/Utilities/PlayReport/Delegates.cs new file mode 100644 index 000000000..7c8952e18 --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport/Delegates.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace Ryujinx.Ava.Utilities.PlayReport +{ + /// + /// 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 FormattedValue ValueFormatter(Value value); + + /// + /// The delegate type that powers multiple value formatters.
+ /// Takes in the result values 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 FormattedValue MultiValueFormatter(Value[] value); + + /// + /// The delegate type that powers multiple value formatters. + /// The dictionary passed to this delegate is sparsely populated; + /// that is, not every key specified in the Play Report needs to match for this to be used.
+ /// Takes in the result values 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 FormattedValue SparseMultiValueFormatter(Dictionary values); +} diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs index 25457744e..ae954c81c 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs @@ -1,6 +1,4 @@ -using static Ryujinx.Ava.Utilities.PlayReport.Analyzer; - -namespace Ryujinx.Ava.Utilities.PlayReport +namespace Ryujinx.Ava.Utilities.PlayReport { public static class PlayReports { @@ -10,7 +8,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport spec => spec .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) // reset to normal status when switching between normal & master mode in title screen - .AddValueFormatter("AoCVer", FormattedValue.AlwaysResets) + .AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets) ) .AddSpec( "0100f2c0115b6000", diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs new file mode 100644 index 000000000..649813b7a --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs @@ -0,0 +1,140 @@ +using FluentAvalonia.Core; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Ava.Utilities.PlayReport +{ + /// + /// 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; } = []; + public List SparseMultiValueFormatters { 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) + => AddValueFormatter(SimpleValueFormatters.Count, reportKey, 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 GameSpec AddValueFormatter(int priority, string reportKey, + ValueFormatter valueFormatter) + { + SimpleValueFormatters.Add(new FormatterSpec + { + Priority = priority, ReportKey = reportKey, Formatter = 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) + => AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter); + + /// + /// 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, Formatter = 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 'Sparse' multi-value formatters do not require every key to be present. + /// If you need this requirement, use . + ///
+ /// The key names to match. + /// The function which can format the values. + /// The current , for chaining convenience. + public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter) + => AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter); + + /// + /// 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 'Sparse' multi-value formatters do not require every key to be present. + /// If you need this requirement, use . + ///
+ /// 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 AddSparseMultiValueFormatter(int priority, string[] reportKeys, + SparseMultiValueFormatter valueFormatter) + { + SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec + { + Priority = priority, ReportKeys = reportKeys, Formatter = 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 Formatter { 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 Formatter { get; init; } + } + + /// + /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their sparsely populated potential values. + /// + public struct SparseMultiFormatterSpec + { + public required int Priority { get; init; } + public required string[] ReportKeys { get; init; } + public SparseMultiValueFormatter Formatter { get; init; } + } +} diff --git a/src/Ryujinx/Utilities/PlayReport/Value.cs b/src/Ryujinx/Utilities/PlayReport/Value.cs new file mode 100644 index 000000000..46d47366d --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport/Value.cs @@ -0,0 +1,130 @@ +using MsgPack; +using Ryujinx.Ava.Utilities.AppLibrary; +using System; + +namespace Ryujinx.Ava.Utilities.PlayReport +{ + /// + /// 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(); + + public override string ToString() + { + object boxed = BoxedValue; + return boxed == null + ? "null" + : boxed.ToString(); + } + + #region AsX accessors + + public bool BooleanValue => PackedValue.AsBoolean(); + public byte ByteValue => PackedValue.AsByte(); + public sbyte SByteValue => PackedValue.AsSByte(); + public short ShortValue => PackedValue.AsInt16(); + public ushort UShortValue => PackedValue.AsUInt16(); + public int IntValue => PackedValue.AsInt32(); + public uint UIntValue => PackedValue.AsUInt32(); + public long LongValue => PackedValue.AsInt64(); + public ulong ULongValue => PackedValue.AsUInt64(); + public float FloatValue => PackedValue.AsSingle(); + public double DoubleValue => PackedValue.AsDouble(); + public string StringValue => PackedValue.AsString(); + public Span BinaryValue => PackedValue.AsBinary(); + + #endregion + } + + /// + /// 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; + + public override string ToString() + { + if (!Handled) + return ""; + + if (Reset) + return ""; + + return FormattedString; + } + + /// + /// 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 SingleAlwaysResets = _ => ForceReset; + + /// + /// A delegate singleton you can use to always return in a . + /// + public static readonly MultiValueFormatter MultiAlwaysResets = _ => 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; + } +}