diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index 4ea413ea1..f1ef1448f 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -135,7 +135,7 @@ namespace Ryujinx.Ava if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport); + PlayReportAnalyzer.FormattedValue value = PlayReport.Analyzer.FormatPlayReportValue(TitleIDs.CurrentApplication.Value, _currentApp, playReport); if (!value.Handled) return; diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs index 9049dec1b..c8649bcc0 100644 --- a/src/Ryujinx/Utilities/PlayReport.cs +++ b/src/Ryujinx/Utilities/PlayReport.cs @@ -1,10 +1,4 @@ -using Gommon; -using MsgPack; -using Ryujinx.Ava.Utilities.AppLibrary; -using Ryujinx.Common.Helper; -using System; -using System.Collections.Generic; -using System.Linq; +using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue; namespace Ryujinx.Ava.Utilities { @@ -24,23 +18,23 @@ namespace Ryujinx.Ava.Utilities spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode) ) .AddSpec( - "010075000ECBE000", + "010075000ecbe000", spec => spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode) ) .AddSpec( - "010028600EBDA000", + "010028600ebda000", spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) ) .AddSpec( // Global & China IDs - ["0100152000022000", "010075100E8EC000"], + ["0100152000022000", "010075100e8ec000"], spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode) ); - private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value) + private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value) => value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset; - private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(ref PlayReportValue value) => + private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) => value.PackedValue.AsDouble() switch { > 800d => "Exploring the Sky Islands", @@ -48,16 +42,16 @@ namespace Ryujinx.Ava.Utilities _ => "Roaming Hyrule" }; - private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value) + private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value) => value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode"; - private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value) + private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value) => value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式"; - private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value) + private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value) => value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury"; - private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value) + private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value) => value.BoxedValue switch { // Single Player diff --git a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs b/src/Ryujinx/Utilities/PlayReportAnalyzer.cs index 3760204a7..e2a520907 100644 --- a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs +++ b/src/Ryujinx/Utilities/PlayReportAnalyzer.cs @@ -3,127 +3,255 @@ using MsgPack; using Ryujinx.Ava.Utilities.AppLibrary; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Ryujinx.Ava.Utilities { + /// + /// The entrypoint for the Play Report analysis system. + /// public class PlayReportAnalyzer { private readonly List _specs = []; + /// + /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration. + /// + /// The ID of the game to listen to Play Reports in. + /// The configuration function for the analysis spec. + /// The current , for chaining convenience. public PlayReportAnalyzer AddSpec(string titleId, Func transform) { + Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _), + $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}."); + _specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] })); return this; } - + + /// + /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration. + /// + /// The ID of the game to listen to Play Reports in. + /// The configuration function for the analysis spec. + /// The current , for chaining convenience. public PlayReportAnalyzer AddSpec(string titleId, Action transform) { + Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _), + $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}."); + _specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform)); return this; } - - public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Func transform) + + /// + /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration. + /// + /// The IDs of the games to listen to Play Reports in. + /// The configuration function for the analysis spec. + /// The current , for chaining convenience. + public PlayReportAnalyzer AddSpec(IEnumerable titleIds, + Func transform) { - _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] })); + string[] tids = titleIds.ToArray(); + Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)), + $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}."); + + _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] })); return this; } - + + /// + /// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration. + /// + /// The IDs of the games to listen to Play Reports in. + /// The configuration function for the analysis spec. + /// The current , for chaining convenience. public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Action transform) { - _specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform)); + string[] tids = titleIds.ToArray(); + Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)), + $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}."); + + _specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform)); return this; } - public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport) + + /// + /// 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. + /// The Play Report received from HLE. + /// A struct representing a possible formatted value. + public FormattedValue FormatPlayReportValue( + string runningGameId, + ApplicationMetadata appMeta, + MessagePackObject playReport + ) { - if (!playReport.IsDictionary) - return PlayReportFormattedValue.Unhandled; + if (!playReport.IsDictionary) + return FormattedValue.Unhandled; if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec)) - return PlayReportFormattedValue.Unhandled; + return FormattedValue.Unhandled; - foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority)) + foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) { if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) continue; - PlayReportValue value = new() + return formatSpec.ValueFormatter(new PlayReportValue { - Application = appMeta, - PackedValue = valuePackObject - }; - - return formatSpec.ValueFormatter(ref value); + Application = appMeta, PackedValue = valuePackObject + }); } - - return PlayReportFormattedValue.Unhandled; + + return FormattedValue.Unhandled; + } + + /// + /// A potential formatted value returned by a . + /// + public 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 PlayReportValueFormatter AlwaysResets = _ => ForceReset; } - } + /// + /// A mapping of title IDs to value formatter specs. + /// + /// Generally speaking, use the .AddSpec(...) methods instead of creating this class yourself. + /// public class PlayReportGameSpec { public required string[] TitleIds { get; init; } - public List Analyses { get; } = []; + public List SimpleValueFormatters { 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 PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter) { - Analyses.Add(new PlayReportValueFormatterSpec + SimpleValueFormatters.Add(new FormatterSpec { - Priority = Analyses.Count, - ReportKey = reportKey, - ValueFormatter = valueFormatter + Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter }); return this; } - - public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter 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 PlayReportGameSpec AddValueFormatter(int priority, string reportKey, + PlayReportValueFormatter valueFormatter) { - Analyses.Add(new PlayReportValueFormatterSpec + SimpleValueFormatters.Add(new FormatterSpec { - Priority = priority, - ReportKey = reportKey, - ValueFormatter = valueFormatter + Priority = priority, ReportKey = reportKey, 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 PlayReportValueFormatter ValueFormatter { get; init; } + } } - public readonly struct PlayReportValue + /// + /// The input data to a , + /// containing the currently running application's , + /// and the matched from the Play Report. + /// + public class PlayReportValue { + /// + /// 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 and the AsX (where X is a numerical type name i.e. Int32) methods for that. + ///
public object BoxedValue => PackedValue.ToObject(); } - public struct PlayReportFormattedValue - { - public bool Handled { get; private init; } - - public bool Reset { get; private init; } - - public string FormattedString { get; private init; } - - public static implicit operator PlayReportFormattedValue(string formattedValue) - => new() { Handled = true, FormattedString = formattedValue }; - - public static PlayReportFormattedValue Unhandled => default; - public static PlayReportFormattedValue ForceReset => new() { Handled = true, Reset = true }; - - public static PlayReportValueFormatter AlwaysResets = AlwaysResetsImpl; - - private static PlayReportFormattedValue AlwaysResetsImpl(ref PlayReportValue _) => ForceReset; - } - - public struct PlayReportValueFormatterSpec - { - public required int Priority { get; init; } - public required string ReportKey { get; init; } - public PlayReportValueFormatter ValueFormatter { get; init; } - } - - public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value); + /// + /// The delegate type that powers the entire analysis system (as it currently is).
+ /// 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 PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value); }