From 1d88771d1b2e90d78a938e5235d13b3a157dad5f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 8 Feb 2025 00:22:34 -0600 Subject: [PATCH 1/3] Play Report Analyzer v4 You can now access the *entire* play report data in any given value formatter. The input types have been restructured and, notably, not every instance of Value has an ApplicationMetadata on it. It's now on the container type that also contains the matched values and the entire play report. --- src/Ryujinx.Horizon/HorizonStatic.cs | 5 +- src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs | 24 ++--- src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs | 24 +++++ src/Ryujinx/DiscordIntegrationModule.cs | 3 +- src/Ryujinx/Utilities/PlayReport/Analyzer.cs | 37 +++++--- src/Ryujinx/Utilities/PlayReport/Delegates.cs | 10 +-- .../Utilities/PlayReport/MatchedValues.cs | 87 +++++++++++++++++++ .../Utilities/PlayReport/PlayReports.cs | 32 +++---- src/Ryujinx/Utilities/PlayReport/Value.cs | 23 +++-- 9 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs create mode 100644 src/Ryujinx/Utilities/PlayReport/MatchedValues.cs diff --git a/src/Ryujinx.Horizon/HorizonStatic.cs b/src/Ryujinx.Horizon/HorizonStatic.cs index 15689f0c8..eb9dd4e31 100644 --- a/src/Ryujinx.Horizon/HorizonStatic.cs +++ b/src/Ryujinx.Horizon/HorizonStatic.cs @@ -1,5 +1,6 @@ using MsgPack; using Ryujinx.Horizon.Common; +using Ryujinx.Horizon.Prepo.Types; using Ryujinx.Memory; using System; using System.Threading; @@ -8,7 +9,7 @@ namespace Ryujinx.Horizon { public static class HorizonStatic { - internal static void HandlePlayReport(MessagePackObject report) => + internal static void HandlePlayReport(PlayReport report) => new Thread(() => PlayReport?.Invoke(report)) { Name = "HLE.PlayReportEvent", @@ -16,7 +17,7 @@ namespace Ryujinx.Horizon Priority = ThreadPriority.AboveNormal }.Start(); - public static event Action PlayReport; + public static event Action PlayReport; [field: ThreadStatic] public static HorizonOptions Options { get; private set; } diff --git a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs index 2f8657e0b..0ca851e6e 100644 --- a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs +++ b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs @@ -1,4 +1,3 @@ -using Gommon; using MsgPack; using MsgPack.Serialization; using Ryujinx.Common.Logging; @@ -12,19 +11,12 @@ using Ryujinx.Horizon.Sdk.Sf; using Ryujinx.Horizon.Sdk.Sf.Hipc; using System; using System.Text; -using System.Threading; using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId; namespace Ryujinx.Horizon.Prepo.Ipc { partial class PrepoService : IPrepoService { - enum PlayReportKind - { - Normal, - System, - } - private readonly ArpApi _arp; private readonly PrepoServicePermissionLevel _permissionLevel; private ulong _systemSessionId; @@ -196,10 +188,17 @@ namespace Ryujinx.Horizon.Prepo.Ipc { return PrepoResult.InvalidBufferSize; } - + StringBuilder builder = new(); MessagePackObject deserializedReport = MessagePackSerializer.UnpackMessagePackObject(reportBuffer.ToArray()); + PlayReport playReport = new() + { + Kind = playReportKind, + Room = gameRoom, + ReportData = deserializedReport + }; + builder.AppendLine(); builder.AppendLine("PlayReport log:"); builder.AppendLine($" Kind: {playReportKind}"); @@ -209,10 +208,12 @@ namespace Ryujinx.Horizon.Prepo.Ipc if (pid != 0) { builder.AppendLine($" Pid: {pid}"); + playReport.Pid = pid; } else { builder.AppendLine($" ApplicationId: {applicationId}"); + playReport.AppId = applicationId; } Result result = _arp.GetApplicationInstanceId(out ulong applicationInstanceId, pid); @@ -223,17 +224,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc _arp.GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, applicationInstanceId).AbortOnFailure(); + playReport.Version = applicationLaunchProperty.Version; + builder.AppendLine($" ApplicationVersion: {applicationLaunchProperty.Version}"); if (!userId.IsNull) { builder.AppendLine($" UserId: {userId}"); + playReport.UserId = userId; } builder.AppendLine($" Room: {gameRoom}"); builder.AppendLine($" Report: {MessagePackObjectFormatter.Format(deserializedReport)}"); - HorizonStatic.HandlePlayReport(deserializedReport); + HorizonStatic.HandlePlayReport(playReport); Logger.Info?.Print(LogClass.ServicePrepo, builder.ToString()); diff --git a/src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs b/src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs new file mode 100644 index 000000000..e896219d5 --- /dev/null +++ b/src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs @@ -0,0 +1,24 @@ +using MsgPack; +using Ryujinx.Horizon.Sdk.Account; +using Ryujinx.Horizon.Sdk.Ncm; + +namespace Ryujinx.Horizon.Prepo.Types +{ + public struct PlayReport + { + public PlayReportKind Kind { get; init; } + public string Room { get; init; } + public MessagePackObject ReportData { get; init; } + + public ApplicationId? AppId; + public ulong? Pid; + public uint Version; + public Uid? UserId; + } + + public enum PlayReportKind + { + Normal, + System, + } +} diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 229b6ee09..1f820a223 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -10,6 +10,7 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE; using Ryujinx.HLE.Loaders.Processes; using Ryujinx.Horizon; +using Ryujinx.Horizon.Prepo.Types; using System.Linq; using System.Text; @@ -124,7 +125,7 @@ namespace Ryujinx.Ava _currentApp = null; } - private static void HandlePlayReport(MessagePackObject playReport) + private static void HandlePlayReport(PlayReport playReport) { if (_discordClient is null) return; if (!TitleIDs.CurrentApplication.Value.HasValue) return; diff --git a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs index 338c198a1..0b4130da5 100644 --- a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs +++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs @@ -85,7 +85,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// - /// Runs the configured for the specified game title ID. + /// 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. @@ -94,10 +94,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport public FormattedValue Format( string runningGameId, ApplicationMetadata appMeta, - MessagePackObject playReport + Horizon.Prepo.Types.PlayReport playReport ) { - if (!playReport.IsDictionary) + if (!playReport.ReportData.IsDictionary) return FormattedValue.Unhandled; if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec)) @@ -105,10 +105,14 @@ namespace Ryujinx.Ava.Utilities.PlayReport foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) { - if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) + if (!playReport.ReportData.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) continue; - return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject }); + return formatSpec.Formatter(new SingleValue(valuePackObject) + { + Application = appMeta, + PlayReport = playReport + }); } foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority)) @@ -116,7 +120,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport List packedObjects = []; foreach (var reportKey in formatSpec.ReportKeys) { - if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) continue; packedObjects.Add(valuePackObject); @@ -125,23 +129,30 @@ namespace Ryujinx.Ava.Utilities.PlayReport if (packedObjects.Count != formatSpec.ReportKeys.Length) return FormattedValue.Unhandled; - return formatSpec.Formatter(packedObjects - .Select(packObject => new Value { Application = appMeta, PackedValue = packObject }) - .ToArray()); + return formatSpec.Formatter(new MultiValue(packedObjects) + { + Application = appMeta, + PlayReport = playReport + }); } foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority)) { - Dictionary packedObjects = []; + Dictionary packedObjects = []; foreach (var reportKey in formatSpec.ReportKeys) { - if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) continue; - packedObjects.Add(reportKey, new Value { Application = appMeta, PackedValue = valuePackObject }); + packedObjects.Add(reportKey, valuePackObject); } - return formatSpec.Formatter(packedObjects); + return formatSpec.Formatter( + new SparseMultiValue(packedObjects) + { + Application = appMeta, + PlayReport = playReport + }); } return FormattedValue.Unhandled; diff --git a/src/Ryujinx/Utilities/PlayReport/Delegates.cs b/src/Ryujinx/Utilities/PlayReport/Delegates.cs index 7c8952e18..789d408d7 100644 --- a/src/Ryujinx/Utilities/PlayReport/Delegates.cs +++ b/src/Ryujinx/Utilities/PlayReport/Delegates.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Ryujinx.Ava.Utilities.PlayReport +namespace Ryujinx.Ava.Utilities.PlayReport { /// /// The delegate type that powers single value formatters.
@@ -12,7 +10,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport ///
/// OR a signal to reset the value that the caller is using the for. ///
- public delegate FormattedValue ValueFormatter(Value value); + public delegate FormattedValue ValueFormatter(SingleValue value); /// /// The delegate type that powers multiple value formatters.
@@ -24,7 +22,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport ///
/// OR a signal to reset the value that the caller is using the for. ///
- public delegate FormattedValue MultiValueFormatter(Value[] value); + public delegate FormattedValue MultiValueFormatter(MultiValue value); /// /// The delegate type that powers multiple value formatters. @@ -38,5 +36,5 @@ namespace Ryujinx.Ava.Utilities.PlayReport ///
/// OR a signal to reset the value that the caller is using the for. ///
- public delegate FormattedValue SparseMultiValueFormatter(Dictionary values); + public delegate FormattedValue SparseMultiValueFormatter(SparseMultiValue value); } diff --git a/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs new file mode 100644 index 000000000..01c404c31 --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs @@ -0,0 +1,87 @@ +using MsgPack; +using Ryujinx.Ava.Utilities.AppLibrary; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Ava.Utilities.PlayReport +{ + public abstract class MatchedValue + { + public MatchedValue(T matched) + { + Matched = matched; + } + + /// + /// The currently running application's . + /// + public ApplicationMetadata Application { get; init; } + + /// + /// The entire play report. + /// + public Horizon.Prepo.Types.PlayReport PlayReport { get; init; } + + /// + /// The matched value from the Play Report. + /// + public T Matched { get; init; } + } + + /// + /// The input data to a , + /// containing the currently running application's , + /// and the matched from the Play Report. + /// + public class SingleValue : MatchedValue + { + public SingleValue(Value matched) : base(matched) + { + } + + public static implicit operator SingleValue(MessagePackObject mpo) => new(mpo); + } + + /// + /// The input data to a , + /// containing the currently running application's , + /// and the matched s from the Play Report. + /// + public class MultiValue : MatchedValue + { + public MultiValue(Value[] matched) : base(matched) + { + } + + public MultiValue(IEnumerable matched) : base(Value.ConvertPackedObjects(matched)) + { + } + + public static implicit operator MultiValue(List matched) + => new(matched.Select(x => new Value(x)).ToArray()); + } + + /// + /// The input data to a , + /// containing the currently running application's , + /// and the matched s from the Play Report. + /// + public class SparseMultiValue : MatchedValue> + { + public SparseMultiValue(Dictionary matched) : base(matched) + { + } + + public SparseMultiValue(Dictionary matched) : base(Value.ConvertPackedObjectMap(matched)) + { + } + + public static implicit operator SparseMultiValue(Dictionary matched) + => new(matched + .ToDictionary( + x => x.Key, + x => new Value(x.Value) + ) + ); + } +} diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs index ae954c81c..9e22cd6d2 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs @@ -39,28 +39,28 @@ .AddValueFormatter("team_circle", PokemonSVUnionCircle) ); - private static FormattedValue BreathOfTheWild_MasterMode(Value value) - => value.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset; + private static FormattedValue BreathOfTheWild_MasterMode(SingleValue value) + => value.Matched.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset; - private static FormattedValue TearsOfTheKingdom_CurrentField(Value value) => - value.DoubleValue switch + private static FormattedValue TearsOfTheKingdom_CurrentField(SingleValue value) => + value.Matched.DoubleValue switch { > 800d => "Exploring the Sky Islands", < -201d => "Exploring the Depths", _ => "Roaming Hyrule" }; - private static FormattedValue SuperMarioOdyssey_AssistMode(Value value) - => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; + private static FormattedValue SuperMarioOdyssey_AssistMode(SingleValue value) + => value.Matched.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; - private static FormattedValue SuperMarioOdysseyChina_AssistMode(Value value) - => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; + private static FormattedValue SuperMarioOdysseyChina_AssistMode(SingleValue value) + => value.Matched.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; - private static FormattedValue SuperMario3DWorldOrBowsersFury(Value value) - => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; + private static FormattedValue SuperMario3DWorldOrBowsersFury(SingleValue value) + => value.Matched.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; - private static FormattedValue MarioKart8Deluxe_Mode(Value value) - => value.StringValue switch + private static FormattedValue MarioKart8Deluxe_Mode(SingleValue value) + => value.Matched.StringValue switch { // Single Player "Single" => "Single Player", @@ -87,11 +87,11 @@ _ => FormattedValue.ForceReset }; - private static FormattedValue PokemonSVUnionCircle(Value value) - => value.BoxedValue is 0 ? "Playing Alone" : "Playing in a group"; + private static FormattedValue PokemonSVUnionCircle(SingleValue value) + => value.Matched.BoxedValue is 0 ? "Playing Alone" : "Playing in a group"; - private static FormattedValue PokemonSVArea(Value value) - => value.StringValue switch + private static FormattedValue PokemonSVArea(SingleValue value) + => value.Matched.StringValue switch { // Base Game Locations "a_w01" => "South Area One", diff --git a/src/Ryujinx/Utilities/PlayReport/Value.cs b/src/Ryujinx/Utilities/PlayReport/Value.cs index 46d47366d..65d662ea0 100644 --- a/src/Ryujinx/Utilities/PlayReport/Value.cs +++ b/src/Ryujinx/Utilities/PlayReport/Value.cs @@ -1,6 +1,8 @@ using MsgPack; using Ryujinx.Ava.Utilities.AppLibrary; using System; +using System.Collections.Generic; +using System.Linq; namespace Ryujinx.Ava.Utilities.PlayReport { @@ -9,12 +11,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// containing the currently running application's , /// and the matched from the Play Report. /// - public class Value + public readonly struct Value { - /// - /// The currently running application's . - /// - public ApplicationMetadata Application { get; init; } + public Value(MessagePackObject packedValue) + { + PackedValue = packedValue; + } /// /// The matched value from the Play Report. @@ -37,6 +39,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport : boxed.ToString(); } + public static implicit operator Value(MessagePackObject matched) => new(matched); + + public static Value[] ConvertPackedObjects(IEnumerable packObjects) + => packObjects.Select(packObject => new Value(packObject)).ToArray(); + + public static Dictionary ConvertPackedObjectMap(Dictionary packObjects) + => packObjects.ToDictionary( + x => x.Key, + x => new Value(x.Value) + ); + #region AsX accessors public bool BooleanValue => PackedValue.AsBoolean(); From 30a534edcdc7d72882a291448ac2a889848eb18f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 8 Feb 2025 01:26:05 -0600 Subject: [PATCH 2/3] misc: chore: [ci skip] generify Formatter Specs to be able to run formatters of different types at interleaving priorities --- src/Ryujinx/Utilities/PlayReport/Analyzer.cs | 50 +------- src/Ryujinx/Utilities/PlayReport/Specs.cs | 124 +++++++++++++++---- 2 files changed, 105 insertions(+), 69 deletions(-) diff --git a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs index 0b4130da5..668eb526c 100644 --- a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs +++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs @@ -103,56 +103,12 @@ namespace Ryujinx.Ava.Utilities.PlayReport if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec)) return FormattedValue.Unhandled; - foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) + foreach (FormatterSpecBase formatSpec in spec.ValueFormatters.OrderBy(x => x.Priority)) { - if (!playReport.ReportData.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) + if (!formatSpec.Format(appMeta, playReport, out FormattedValue value)) continue; - return formatSpec.Formatter(new SingleValue(valuePackObject) - { - Application = appMeta, - PlayReport = playReport - }); - } - - foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority)) - { - List packedObjects = []; - foreach (var reportKey in formatSpec.ReportKeys) - { - if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) - continue; - - packedObjects.Add(valuePackObject); - } - - if (packedObjects.Count != formatSpec.ReportKeys.Length) - return FormattedValue.Unhandled; - - return formatSpec.Formatter(new MultiValue(packedObjects) - { - Application = appMeta, - PlayReport = playReport - }); - } - - foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority)) - { - Dictionary packedObjects = []; - foreach (var reportKey in formatSpec.ReportKeys) - { - if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) - continue; - - packedObjects.Add(reportKey, valuePackObject); - } - - return formatSpec.Formatter( - new SparseMultiValue(packedObjects) - { - Application = appMeta, - PlayReport = playReport - }); + return value; } return FormattedValue.Unhandled; diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs index 649813b7a..3c80198b9 100644 --- a/src/Ryujinx/Utilities/PlayReport/Specs.cs +++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs @@ -1,4 +1,7 @@ using FluentAvalonia.Core; +using MsgPack; +using Ryujinx.Ava.Utilities.AppLibrary; +using System; using System.Collections.Generic; using System.Linq; @@ -11,10 +14,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// public class GameSpec { + private int _lastPriority; + public required string[] TitleIds { get; init; } - public List SimpleValueFormatters { get; } = []; - public List MultiValueFormatters { get; } = []; - public List SparseMultiValueFormatters { get; } = []; + + public List ValueFormatters { get; } = []; /// @@ -25,7 +29,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// 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); + => AddValueFormatter(_lastPriority++, reportKey, valueFormatter); /// /// Add a value formatter at a specific priority to the current @@ -38,9 +42,9 @@ namespace Ryujinx.Ava.Utilities.PlayReport public GameSpec AddValueFormatter(int priority, string reportKey, ValueFormatter valueFormatter) { - SimpleValueFormatters.Add(new FormatterSpec + ValueFormatters.Add(new FormatterSpec { - Priority = priority, ReportKey = reportKey, Formatter = valueFormatter + Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter }); return this; } @@ -53,7 +57,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// The function which can format the values. /// The current , for chaining convenience. public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter) - => AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter); + => AddMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter); /// /// Add a multi-value formatter at a specific priority to the current @@ -66,7 +70,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys, MultiValueFormatter valueFormatter) { - MultiValueFormatters.Add(new MultiFormatterSpec + ValueFormatters.Add(new MultiFormatterSpec { Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter }); @@ -84,7 +88,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// The function which can format the values. /// The current , for chaining convenience. public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter) - => AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter); + => AddSparseMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter); /// /// Add a multi-value formatter at a specific priority to the current @@ -100,7 +104,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys, SparseMultiValueFormatter valueFormatter) { - SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec + ValueFormatters.Add(new SparseMultiFormatterSpec { Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter }); @@ -111,30 +115,106 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// /// 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 class FormatterSpec : FormatterSpecBase { - public required int Priority { get; init; } - public required string ReportKey { get; init; } - public ValueFormatter Formatter { get; init; } + public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) + { + if (!playReport.ReportData.AsDictionary().TryGetValue(ReportKeys[0], out MessagePackObject valuePackObject)) + { + result = null; + return false; + } + + result = valuePackObject; + return true; + } } /// /// 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 class MultiFormatterSpec : FormatterSpecBase { - public required int Priority { get; init; } - public required string[] ReportKeys { get; init; } - public MultiValueFormatter Formatter { get; init; } + public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) + { + List packedObjects = []; + foreach (var reportKey in ReportKeys) + { + if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + { + result = null; + return false; + } + + packedObjects.Add(valuePackObject); + } + + result = packedObjects; + return true; + } } /// /// 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 class SparseMultiFormatterSpec : FormatterSpecBase { - public required int Priority { get; init; } - public required string[] ReportKeys { get; init; } - public SparseMultiValueFormatter Formatter { get; init; } + public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result) + { + Dictionary packedObjects = []; + foreach (var reportKey in ReportKeys) + { + if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + continue; + + packedObjects.Add(reportKey, valuePackObject); + } + + result = packedObjects; + return true; + } + } + + public abstract class FormatterSpecBase + { + public abstract bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object data); + + public int Priority { get; init; } + public string[] ReportKeys { get; init; } + public Delegate Formatter { get; init; } + + public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport, out FormattedValue formattedValue) + { + formattedValue = default; + if (!GetData(playReport, out object data)) + return false; + + if (data is FormattedValue fv) + { + formattedValue = fv; + return true; + } + + if (Formatter is ValueFormatter vf && data is MessagePackObject mpo) + { + formattedValue = vf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport }); + return true; + } + + if (Formatter is MultiValueFormatter mvf && data is List messagePackObjects) + { + formattedValue = mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); + return true; + } + + if (Formatter is SparseMultiValueFormatter smvf && + data is Dictionary sparseMessagePackObjects) + { + formattedValue = smvf(new SparseMultiValue(sparseMessagePackObjects) { Application = appMeta, PlayReport = playReport }); + return true; + } + + throw new InvalidOperationException("Formatter delegate is not of a known type!"); + } } } From 9c226dcc7a92d1068509db6edecc3a8f24cbd097 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 8 Feb 2025 01:34:44 -0600 Subject: [PATCH 3/3] misc: chore: [ci skip] rename ValueFormatter to SingleValueFormatter and some minor cleanups --- src/Ryujinx/Utilities/PlayReport/Delegates.cs | 2 +- .../Utilities/PlayReport/MatchedValues.cs | 17 +-------- src/Ryujinx/Utilities/PlayReport/Specs.cs | 35 ++++++++---------- src/Ryujinx/Utilities/PlayReport/Value.cs | 37 ++++++++++++++----- 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/Ryujinx/Utilities/PlayReport/Delegates.cs b/src/Ryujinx/Utilities/PlayReport/Delegates.cs index 789d408d7..92569d32e 100644 --- a/src/Ryujinx/Utilities/PlayReport/Delegates.cs +++ b/src/Ryujinx/Utilities/PlayReport/Delegates.cs @@ -10,7 +10,7 @@ ///
/// OR a signal to reset the value that the caller is using the for. ///
- public delegate FormattedValue ValueFormatter(SingleValue value); + public delegate FormattedValue SingleValueFormatter(SingleValue value); /// /// The delegate type that powers multiple value formatters.
diff --git a/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs index 01c404c31..3086a9d65 100644 --- a/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs +++ b/src/Ryujinx/Utilities/PlayReport/MatchedValues.cs @@ -7,7 +7,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport { public abstract class MatchedValue { - public MatchedValue(T matched) + protected MatchedValue(T matched) { Matched = matched; } @@ -29,7 +29,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport } /// - /// The input data to a , + /// The input data to a , /// containing the currently running application's , /// and the matched from the Play Report. /// @@ -38,8 +38,6 @@ namespace Ryujinx.Ava.Utilities.PlayReport public SingleValue(Value matched) : base(matched) { } - - public static implicit operator SingleValue(MessagePackObject mpo) => new(mpo); } /// @@ -56,9 +54,6 @@ namespace Ryujinx.Ava.Utilities.PlayReport public MultiValue(IEnumerable matched) : base(Value.ConvertPackedObjects(matched)) { } - - public static implicit operator MultiValue(List matched) - => new(matched.Select(x => new Value(x)).ToArray()); } /// @@ -75,13 +70,5 @@ namespace Ryujinx.Ava.Utilities.PlayReport public SparseMultiValue(Dictionary matched) : base(Value.ConvertPackedObjectMap(matched)) { } - - public static implicit operator SparseMultiValue(Dictionary matched) - => new(matched - .ToDictionary( - x => x.Key, - x => new Value(x.Value) - ) - ); } } diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs index 3c80198b9..e7972fbb4 100644 --- a/src/Ryujinx/Utilities/PlayReport/Specs.cs +++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs @@ -28,7 +28,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// 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) + public GameSpec AddValueFormatter(string reportKey, SingleValueFormatter valueFormatter) => AddValueFormatter(_lastPriority++, reportKey, valueFormatter); /// @@ -40,7 +40,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// The function which can return a potential formatted value. /// The current , for chaining convenience. public GameSpec AddValueFormatter(int priority, string reportKey, - ValueFormatter valueFormatter) + SingleValueFormatter valueFormatter) { ValueFormatters.Add(new FormatterSpec { @@ -195,26 +195,21 @@ namespace Ryujinx.Ava.Utilities.PlayReport return true; } - if (Formatter is ValueFormatter vf && data is MessagePackObject mpo) + switch (Formatter) { - formattedValue = vf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport }); - return true; + case SingleValueFormatter svf when data is MessagePackObject mpo: + formattedValue = svf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport }); + return true; + case MultiValueFormatter mvf when data is List messagePackObjects: + formattedValue = mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); + return true; + case SparseMultiValueFormatter smvf when + data is Dictionary sparseMessagePackObjects: + formattedValue = smvf(new SparseMultiValue(sparseMessagePackObjects) { Application = appMeta, PlayReport = playReport }); + return true; + default: + throw new InvalidOperationException("Formatter delegate is not of a known type!"); } - - if (Formatter is MultiValueFormatter mvf && data is List messagePackObjects) - { - formattedValue = mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport }); - return true; - } - - if (Formatter is SparseMultiValueFormatter smvf && - data is Dictionary sparseMessagePackObjects) - { - formattedValue = smvf(new SparseMultiValue(sparseMessagePackObjects) { Application = appMeta, PlayReport = playReport }); - return true; - } - - throw new InvalidOperationException("Formatter delegate is not of a known type!"); } } } diff --git a/src/Ryujinx/Utilities/PlayReport/Value.cs b/src/Ryujinx/Utilities/PlayReport/Value.cs index 65d662ea0..b3108a41e 100644 --- a/src/Ryujinx/Utilities/PlayReport/Value.cs +++ b/src/Ryujinx/Utilities/PlayReport/Value.cs @@ -1,5 +1,4 @@ using MsgPack; -using Ryujinx.Ava.Utilities.AppLibrary; using System; using System.Collections.Generic; using System.Linq; @@ -7,8 +6,7 @@ using System.Linq; namespace Ryujinx.Ava.Utilities.PlayReport { /// - /// The input data to a , - /// containing the currently running application's , + /// The base input data to a ValueFormatter delegate, /// and the matched from the Play Report. /// public readonly struct Value @@ -70,7 +68,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport } /// - /// A potential formatted value returned by a . + /// A potential formatted value returned by a ValueFormatter delegate. /// public readonly struct FormattedValue { @@ -116,28 +114,47 @@ namespace Ryujinx.Ava.Utilities.PlayReport /// /// Return this to tell the caller there is no value to return. /// - public static FormattedValue Unhandled => default; + public static readonly 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 }; + public static readonly FormattedValue ForceReset = new() { Handled = true, Reset = true }; /// - /// A delegate singleton you can use to always return in a . + /// A delegate singleton you can use to always return in a . /// - public static readonly ValueFormatter SingleAlwaysResets = _ => ForceReset; + public static readonly SingleValueFormatter SingleAlwaysResets = _ => ForceReset; /// /// A delegate singleton you can use to always return in a . /// public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset; + + /// + /// A delegate singleton you can use to always return in a . + /// + public static readonly SparseMultiValueFormatter SparseMultiAlwaysResets = _ => ForceReset; /// /// A delegate factory you can use to always return the specified - /// in a . + /// in a . /// /// The string to always return for this delegate instance. - public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue; + public static SingleValueFormatter SingleAlwaysReturns(string formattedValue) => _ => formattedValue; + + /// + /// A delegate factory you can use to always return the specified + /// in a . + /// + /// The string to always return for this delegate instance. + public static MultiValueFormatter MultiAlwaysReturns(string formattedValue) => _ => formattedValue; + + /// + /// A delegate factory you can use to always return the specified + /// in a . + /// + /// The string to always return for this delegate instance. + public static SparseMultiValueFormatter SparseMultiAlwaysReturns(string formattedValue) => _ => formattedValue; } }