Skip to content

Commit

Permalink
Play Report Analyzer v4
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
GreemDev committed Feb 8, 2025
1 parent 4e81576 commit 1d88771
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 53 deletions.
5 changes: 3 additions & 2 deletions src/Ryujinx.Horizon/HorizonStatic.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using MsgPack;
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.Prepo.Types;
using Ryujinx.Memory;
using System;
using System.Threading;
Expand All @@ -8,15 +9,15 @@ 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",
IsBackground = true,
Priority = ThreadPriority.AboveNormal
}.Start();

public static event Action<MessagePackObject> PlayReport;
public static event Action<PlayReport> PlayReport;

[field: ThreadStatic]
public static HorizonOptions Options { get; private set; }
Expand Down
24 changes: 14 additions & 10 deletions src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Gommon;
using MsgPack;
using MsgPack.Serialization;
using Ryujinx.Common.Logging;
Expand All @@ -12,19 +11,12 @@
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;
Expand Down Expand Up @@ -196,10 +188,17 @@ private Result ProcessPlayReport(PlayReportKind playReportKind, ReadOnlySpan<byt
{
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}");
Expand All @@ -209,10 +208,12 @@ private Result ProcessPlayReport(PlayReportKind playReportKind, ReadOnlySpan<byt
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);
Expand All @@ -223,17 +224,20 @@ private Result ProcessPlayReport(PlayReportKind playReportKind, ReadOnlySpan<byt

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

Expand Down
24 changes: 24 additions & 0 deletions src/Ryujinx.Horizon/Prepo/Types/PlayReport.cs
Original file line number Diff line number Diff line change
@@ -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,
}
}
3 changes: 2 additions & 1 deletion src/Ryujinx/DiscordIntegrationModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon;
using Ryujinx.Horizon.Prepo.Types;
using System.Linq;
using System.Text;

Expand Down Expand Up @@ -124,7 +125,7 @@ private static void SwitchToMainState()
_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;
Expand Down
37 changes: 24 additions & 13 deletions src/Ryujinx/Utilities/PlayReport/Analyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public Analyzer AddSpec(IEnumerable<string> titleIds, Action<GameSpec> transform


/// <summary>
/// Runs the configured <see cref="GameSpec.FormatterSpec"/> for the specified game title ID.
/// Runs the configured <see cref="FormatterSpec"/> for the specified game title ID.
/// </summary>
/// <param name="runningGameId">The game currently running.</param>
/// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
Expand All @@ -94,29 +94,33 @@ public Analyzer AddSpec(IEnumerable<string> titleIds, Action<GameSpec> transform
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))
return FormattedValue.Unhandled;

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))
{
List<MessagePackObject> 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);
Expand All @@ -125,23 +129,30 @@ MessagePackObject 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<string, Value> packedObjects = [];
Dictionary<string, MessagePackObject> 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;
Expand Down
10 changes: 4 additions & 6 deletions src/Ryujinx/Utilities/PlayReport/Delegates.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Collections.Generic;

namespace Ryujinx.Ava.Utilities.PlayReport
namespace Ryujinx.Ava.Utilities.PlayReport
{
/// <summary>
/// The delegate type that powers single value formatters.<br/>
Expand All @@ -12,7 +10,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <br/>
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
/// </summary>
public delegate FormattedValue ValueFormatter(Value value);
public delegate FormattedValue ValueFormatter(SingleValue value);

/// <summary>
/// The delegate type that powers multiple value formatters.<br/>
Expand All @@ -24,7 +22,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <br/>
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
/// </summary>
public delegate FormattedValue MultiValueFormatter(Value[] value);
public delegate FormattedValue MultiValueFormatter(MultiValue value);

/// <summary>
/// The delegate type that powers multiple value formatters.
Expand All @@ -38,5 +36,5 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <br/>
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
/// </summary>
public delegate FormattedValue SparseMultiValueFormatter(Dictionary<string, Value> values);
public delegate FormattedValue SparseMultiValueFormatter(SparseMultiValue value);
}
87 changes: 87 additions & 0 deletions src/Ryujinx/Utilities/PlayReport/MatchedValues.cs
Original file line number Diff line number Diff line change
@@ -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<T>
{
public MatchedValue(T matched)
{
Matched = matched;
}

/// <summary>
/// The currently running application's <see cref="ApplicationMetadata"/>.
/// </summary>
public ApplicationMetadata Application { get; init; }

/// <summary>
/// The entire play report.
/// </summary>
public Horizon.Prepo.Types.PlayReport PlayReport { get; init; }

/// <summary>
/// The matched value from the Play Report.
/// </summary>
public T Matched { get; init; }
}

/// <summary>
/// The input data to a <see cref="ValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
/// </summary>
public class SingleValue : MatchedValue<Value>
{
public SingleValue(Value matched) : base(matched)
{
}

public static implicit operator SingleValue(MessagePackObject mpo) => new(mpo);
}

/// <summary>
/// The input data to a <see cref="MultiValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/>s from the Play Report.
/// </summary>
public class MultiValue : MatchedValue<Value[]>
{
public MultiValue(Value[] matched) : base(matched)
{
}

public MultiValue(IEnumerable<MessagePackObject> matched) : base(Value.ConvertPackedObjects(matched))
{
}

public static implicit operator MultiValue(List<MessagePackObject> matched)
=> new(matched.Select(x => new Value(x)).ToArray());
}

/// <summary>
/// The input data to a <see cref="SparseMultiValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/>s from the Play Report.
/// </summary>
public class SparseMultiValue : MatchedValue<Dictionary<string, Value>>
{
public SparseMultiValue(Dictionary<string, Value> matched) : base(matched)
{
}

public SparseMultiValue(Dictionary<string, MessagePackObject> matched) : base(Value.ConvertPackedObjectMap(matched))
{
}

public static implicit operator SparseMultiValue(Dictionary<string, MessagePackObject> matched)
=> new(matched
.ToDictionary(
x => x.Key,
x => new Value(x.Value)
)
);
}
}
Loading

0 comments on commit 1d88771

Please sign in to comment.