diff --git a/src/Ryujinx.Horizon/HorizonStatic.cs b/src/Ryujinx.Horizon/HorizonStatic.cs index f08ddb3c0..15689f0c8 100644 --- a/src/Ryujinx.Horizon/HorizonStatic.cs +++ b/src/Ryujinx.Horizon/HorizonStatic.cs @@ -2,35 +2,36 @@ using MsgPack; using Ryujinx.Horizon.Common; using Ryujinx.Memory; using System; +using System.Threading; namespace Ryujinx.Horizon { public static class HorizonStatic { - internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted?.Invoke(report); + internal static void HandlePlayReport(MessagePackObject report) => + new Thread(() => PlayReport?.Invoke(report)) + { + Name = "HLE.PlayReportEvent", + IsBackground = true, + Priority = ThreadPriority.AboveNormal + }.Start(); - public static event Action PlayReportPrinted; - - [ThreadStatic] - private static HorizonOptions _options; + public static event Action PlayReport; - [ThreadStatic] - private static ISyscallApi _syscall; + [field: ThreadStatic] + public static HorizonOptions Options { get; private set; } - [ThreadStatic] - private static IVirtualMemoryManager _addressSpace; + [field: ThreadStatic] + public static ISyscallApi Syscall { get; private set; } - [ThreadStatic] - private static IThreadContext _threadContext; + [field: ThreadStatic] + public static IVirtualMemoryManager AddressSpace { get; private set; } - [ThreadStatic] - private static int _threadHandle; + [field: ThreadStatic] + public static IThreadContext ThreadContext { get; private set; } - public static HorizonOptions Options => _options; - public static ISyscallApi Syscall => _syscall; - public static IVirtualMemoryManager AddressSpace => _addressSpace; - public static IThreadContext ThreadContext => _threadContext; - public static int CurrentThreadHandle => _threadHandle; + [field: ThreadStatic] + public static int CurrentThreadHandle { get; private set; } public static void Register( HorizonOptions options, @@ -39,11 +40,11 @@ namespace Ryujinx.Horizon IThreadContext threadContext, int threadHandle) { - _options = options; - _syscall = syscallApi; - _addressSpace = addressSpace; - _threadContext = threadContext; - _threadHandle = threadHandle; + Options = options; + Syscall = syscallApi; + AddressSpace = addressSpace; + ThreadContext = threadContext; + CurrentThreadHandle = threadHandle; } } } diff --git a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs index ab972d85e..2f8657e0b 100644 --- a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs +++ b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs @@ -1,3 +1,4 @@ +using Gommon; using MsgPack; using MsgPack.Serialization; using Ryujinx.Common.Logging; @@ -11,6 +12,7 @@ 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 diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 3db41b963..18842ce72 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -23069,7 +23069,7 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "可游玩", - "zh_TW": "可暢順遊玩 (Playable)" + "zh_TW": "可暢順遊玩" } }, { @@ -23094,7 +23094,7 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "进入游戏", - "zh_TW": "大致可遊玩 (Ingame)" + "zh_TW": "大致可遊玩" } }, { @@ -23119,7 +23119,7 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "菜单", - "zh_TW": "只開啟至遊戲開始功能表 (Menus)" + "zh_TW": "只開啟至遊戲開始功能表" } }, { @@ -23144,7 +23144,7 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "启动", - "zh_TW": "只能啟動 (Boots)" + "zh_TW": "只能啟動" } }, { @@ -23169,7 +23169,7 @@ "tr_TR": "", "uk_UA": "", "zh_CN": "什么都没有", - "zh_TW": "無法啟動 (Nothing)" + "zh_TW": "無法啟動" } }, { diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index f55eb8d66..5d55eba42 100644 --- a/src/Ryujinx/DiscordIntegrationModule.cs +++ b/src/Ryujinx/DiscordIntegrationModule.cs @@ -56,7 +56,7 @@ namespace Ryujinx.Ava ConfigurationState.Instance.EnableDiscordIntegration.Event += Update; TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue); - HorizonStatic.PlayReportPrinted += HandlePlayReport; + HorizonStatic.PlayReport += HandlePlayReport; } private static void Update(object sender, ReactiveEventArgs evnt) @@ -135,21 +135,21 @@ namespace Ryujinx.Ava if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport); - - if (!value.Handled) return; - - if (value.Reset) - { - _discordPresencePlaying.Details = $"Playing {_currentApp.Title}"; - Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter."); - } - else - { - _discordPresencePlaying.Details = value.FormattedString; - Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); - } - UpdatePlayingState(); + PlayReport.Analyzer.FormatPlayReportValue(TitleIDs.CurrentApplication.Value, _currentApp, playReport) + .Match(out bool handled, + () => + { + _discordPresencePlaying.Details = $"Playing {_currentApp.Title}"; + Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter."); + }, + formattedString => + { + _discordPresencePlaying.Details = formattedString; + Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report."); + }); + + if (handled) + UpdatePlayingState(); } private static string TruncateToByteLength(string input) diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index 9b87805e4..af7b5ccd0 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -86,6 +86,13 @@ Text="{Binding Version}" TextAlignment="Start" TextWrapping="Wrap" /> + _id; + set + { + _id = value; + PlayabilityStatus = CompatibilityCsv.GetStatus(Id); + } + } public string Developer { get; set; } = "Unknown"; public string Version { get; set; } = "0"; + + public bool HasPlayabilityInfo => PlayabilityStatus != null; + + public string LocalizedStatus => + PlayabilityStatus.HasValue + ? LocaleManager.Instance[PlayabilityStatus!.Value] + : string.Empty; + + public LocaleKeys? PlayabilityStatus { get; set; } + public int PlayerCount { get; set; } public int GameCount { get; set; } public TimeSpan TimePlayed { get; set; } diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs index af80c5a28..d0e251fe0 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs +++ b/src/Ryujinx/Utilities/Compat/CompatibilityCsv.cs @@ -47,11 +47,6 @@ namespace Ryujinx.Ava.Utilities.Compat Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility"); } - public static void Unload() - { - _entries = null; - } - private static CompatibilityEntry[] _entries; public static CompatibilityEntry[] Entries @@ -64,6 +59,11 @@ namespace Ryujinx.Ava.Utilities.Compat return _entries; } } + + public static LocaleKeys? GetStatus(string titleId) + => Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId))?.Status; + + public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16")); } public class CompatibilityEntry diff --git a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs index 7fc48b187..e0d3b0c56 100644 --- a/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs +++ b/src/Ryujinx/Utilities/Compat/CompatibilityList.axaml.cs @@ -32,8 +32,6 @@ namespace Ryujinx.Ava.Utilities.Compat contentDialog.Styles.Add(closeButtonParent); await ContentDialogHelper.ShowAsync(contentDialog); - - CompatibilityCsv.Unload(); } public CompatibilityList() diff --git a/src/Ryujinx/Utilities/PlayReport.cs b/src/Ryujinx/Utilities/PlayReport.cs index af56bae12..f8e361333 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 { @@ -13,40 +7,54 @@ namespace Ryujinx.Ava.Utilities public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer() .AddSpec( "01007ef00011e000", - spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) + spec => spec + .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) + // reset to normal status when switching between normal & master mode in title screen + .AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets) ) - .AddSpec( // Super Mario Odyssey + .AddSpec( + "0100f2c0115b6000", + spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField)) + .AddSpec( "0100000000010000", spec => spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode) ) - .AddSpec( // Super Mario Odyssey (China) - "010075000ECBE000", + .AddSpec( + "010075000ecbe000", spec => spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode) ) - .AddSpec( // Super Mario 3D World + Bowser's Fury - "010028600EBDA000", + .AddSpec( + "010028600ebda000", spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury) ) - .AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China) - ["0100152000022000", "010075100E8EC000"], + .AddSpec( // Global & China IDs + ["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 SuperMarioOdyssey_AssistMode(ref PlayReportValue value) + private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) => + value.PackedValue.AsDouble() switch + { + > 800d => "Exploring the Sky Islands", + < -201d => "Exploring the Depths", + _ => "Roaming Hyrule" + }; + + 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 @@ -74,125 +82,4 @@ namespace Ryujinx.Ava.Utilities _ => PlayReportFormattedValue.ForceReset }; } - - #region Analyzer implementation - - public class PlayReportAnalyzer - { - private readonly List _specs = []; - - public PlayReportAnalyzer AddSpec(string titleId, Func transform) - { - _specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] })); - return this; - } - - public PlayReportAnalyzer AddSpec(string titleId, Action transform) - { - _specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform)); - return this; - } - - public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Func transform) - { - _specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] })); - return this; - } - - public PlayReportAnalyzer AddSpec(IEnumerable titleIds, Action transform) - { - _specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform)); - return this; - } - - public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport) - { - if (!playReport.IsDictionary) - return PlayReportFormattedValue.Unhandled; - - if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec)) - return PlayReportFormattedValue.Unhandled; - - foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority)) - { - if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) - continue; - - PlayReportValue value = new() - { - Application = appMeta, - BoxedValue = valuePackObject.ToObject() - }; - - return formatSpec.ValueFormatter(ref value); - } - - return PlayReportFormattedValue.Unhandled; - } - - } - - public class PlayReportGameSpec - { - public required string[] TitleIds { get; init; } - public List Analyses { get; } = []; - - public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter) - { - Analyses.Add(new PlayReportValueFormatterSpec - { - Priority = Analyses.Count, - ReportKey = reportKey, - ValueFormatter = valueFormatter - }); - return this; - } - - public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter valueFormatter) - { - Analyses.Add(new PlayReportValueFormatterSpec - { - Priority = priority, - ReportKey = reportKey, - ValueFormatter = valueFormatter - }); - return this; - } - } - - public struct PlayReportValue - { - public ApplicationMetadata Application { get; init; } - public object BoxedValue { get; init; } - } - - 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); - - #endregion } diff --git a/src/Ryujinx/Utilities/PlayReportAnalyzer.cs b/src/Ryujinx/Utilities/PlayReportAnalyzer.cs new file mode 100644 index 000000000..80bef13bc --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReportAnalyzer.cs @@ -0,0 +1,281 @@ +using Gommon; +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; + } + + /// + /// 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) + { + 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) + { + 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; + } + + + /// + /// 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 FormattedValue.Unhandled; + + if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec)) + return FormattedValue.Unhandled; + + foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) + { + if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) + continue; + + return formatSpec.ValueFormatter(new PlayReportValue + { + Application = appMeta, PackedValue = valuePackObject + }); + } + + return FormattedValue.Unhandled; + } + + /// + /// A potential formatted value returned by a . + /// + public readonly 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; } + + public void Match(out bool wasHandled, Action onReset, Action onSuccess) + { + if (!Handled) + { + wasHandled = false; + return; + } + + if (Reset) + onReset(); + else + onSuccess(FormattedString); + + wasHandled = true; + } + + + /// + /// 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 delegate factory you can use to always return the specified + /// in a . + /// The string to always return for this delegate instance. + /// + public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue; + } + } + + /// + /// 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 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) + { + SimpleValueFormatters.Add(new FormatterSpec + { + Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter + }); + return this; + } + + /// + /// 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) + { + SimpleValueFormatters.Add(new FormatterSpec + { + 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; } + } + } + + /// + /// 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(); + } + + /// + /// 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); +}