From 1d88771d1b2e90d78a938e5235d13b3a157dad5f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 8 Feb 2025 00:22:34 -0600 Subject: [PATCH] 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();