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