diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 8330e1123..b88cafa3d 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -23796,6 +23796,56 @@ "zh_CN": "选择一个要解压的 DLC", "zh_TW": "" } + }, + { + "ID": "GameInfoRpcImage", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Rich Presence Image", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "GameInfoRpcDynamic", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Dynamic Rich Presence", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } } ] } \ No newline at end of file diff --git a/src/Ryujinx/DiscordIntegrationModule.cs b/src/Ryujinx/DiscordIntegrationModule.cs index d95bb80dd..229b6ee09 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 System.Linq; using System.Text; namespace Ryujinx.Ava @@ -37,6 +38,9 @@ namespace Ryujinx.Ava private static RichPresence _discordPresencePlaying; private static ApplicationMetadata _currentApp; + public static bool HasAssetImage(string titleId) => TitleIDs.DiscordGameAssetKeys.ContainsIgnoreCase(titleId); + public static bool HasAnalyzer(string titleId) => PlayReports.Analyzer.TitleIds.ContainsIgnoreCase(titleId); + public static void Initialize() { _discordPresenceMain = new RichPresence @@ -126,14 +130,16 @@ namespace Ryujinx.Ava if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (_discordPresencePlaying is null) return; - Analyzer.FormattedValue formattedValue = + FormattedValue formattedValue = PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport); if (!formattedValue.Handled) return; - _discordPresencePlaying.Details = formattedValue.Reset - ? $"Playing {_currentApp.Title}" - : formattedValue.FormattedString; + _discordPresencePlaying.Details = TruncateToByteLength( + formattedValue.Reset + ? $"Playing {_currentApp.Title}" + : formattedValue.FormattedString + ); if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details)) return; //don't trigger an update if the set presence Details are identical to current diff --git a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml index a0b6ad7b3..c40b6e192 100644 --- a/src/Ryujinx/UI/Controls/ApplicationDataView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationDataView.axaml @@ -4,6 +4,7 @@ xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" xmlns:ext="using:Ryujinx.Ava.Common.Markup" xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels" + xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView" @@ -85,6 +86,49 @@ + + + + + + + + + + + + + + + + + + + Brushes.Red, - LocaleKeys.CompatibilityListIngame => Brushes.Yellow, + LocaleKeys.CompatibilityListIngame => Brushes.DarkOrange, _ => Brushes.ForestGreen }; diff --git a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs index 66df0f4c9..ab12156bb 100644 --- a/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs +++ b/src/Ryujinx/Utilities/AppLibrary/ApplicationData.cs @@ -64,6 +64,9 @@ namespace Ryujinx.Ava.Utilities.AppLibrary public int GameCount { get; set; } public bool HasLdnGames => PlayerCount != 0 && GameCount != 0; + + public bool HasRichPresenceAsset => DiscordIntegrationModule.HasAssetImage(IdString); + public bool HasDynamicRichPresenceSupport => DiscordIntegrationModule.HasAnalyzer(IdString); public TimeSpan TimePlayed { get; set; } public DateTime? LastPlayed { get; set; } diff --git a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs index 84bdbf085..338c198a1 100644 --- a/src/Ryujinx/Utilities/PlayReport/Analyzer.cs +++ b/src/Ryujinx/Utilities/PlayReport/Analyzer.cs @@ -3,6 +3,7 @@ using MsgPack; using Ryujinx.Ava.Utilities.AppLibrary; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; @@ -15,6 +16,10 @@ namespace Ryujinx.Ava.Utilities.PlayReport { private readonly List _specs = []; + public string[] TitleIds => Specs.SelectMany(x => x.TitleIds).ToArray(); + + public IReadOnlyList Specs => new ReadOnlyCollection(_specs); + /// /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration. /// @@ -78,7 +83,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport return this; } - + /// /// Runs the configured for the specified game title ID. /// @@ -98,261 +103,48 @@ namespace Ryujinx.Ava.Utilities.PlayReport if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec)) return FormattedValue.Unhandled; - foreach (GameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) + foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) { if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) continue; - return formatSpec.ValueFormatter(new Value - { - Application = appMeta, PackedValue = valuePackObject - }); + return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject }); } - - foreach (GameSpec.MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority)) + + foreach (MultiFormatterSpec formatSpec in spec.MultiValueFormatters.OrderBy(x => x.Priority)) { List packedObjects = []; foreach (var reportKey in formatSpec.ReportKeys) { if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) continue; - + packedObjects.Add(valuePackObject); } - + if (packedObjects.Count != formatSpec.ReportKeys.Length) return FormattedValue.Unhandled; - - return formatSpec.ValueFormatter(packedObjects + + return formatSpec.Formatter(packedObjects .Select(packObject => new Value { Application = appMeta, PackedValue = packObject }) .ToArray()); } + foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority)) + { + Dictionary packedObjects = []; + foreach (var reportKey in formatSpec.ReportKeys) + { + if (!playReport.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject)) + continue; + + packedObjects.Add(reportKey, new Value { Application = appMeta, PackedValue = valuePackObject }); + } + + return formatSpec.Formatter(packedObjects); + } + 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; } - - /// - /// 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 ValueFormatter 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 ValueFormatter 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 GameSpec - { - public required string[] TitleIds { get; init; } - public List SimpleValueFormatters { get; } = []; - public List MultiValueFormatters { 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 GameSpec AddValueFormatter(string reportKey, ValueFormatter 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 GameSpec AddValueFormatter(int priority, string reportKey, - ValueFormatter valueFormatter) - { - SimpleValueFormatters.Add(new FormatterSpec - { - Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter - }); - return this; - } - - /// - /// Add a multi-value formatter to the current - /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs. - /// - /// The key names to match. - /// The function which can format the values. - /// The current , for chaining convenience. - public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter) - { - MultiValueFormatters.Add(new MultiFormatterSpec - { - Priority = SimpleValueFormatters.Count, ReportKeys = reportKeys, ValueFormatter = valueFormatter - }); - return this; - } - - /// - /// Add a multi-value formatter at a specific priority to the current - /// matching a specific set of keys 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 names to match. - /// The function which can format the values. - /// The current , for chaining convenience. - public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys, - MultiValueFormatter valueFormatter) - { - MultiValueFormatters.Add(new MultiFormatterSpec - { - Priority = priority, ReportKeys = reportKeys, 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 ValueFormatter ValueFormatter { get; init; } - } - - /// - /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values. - /// - public struct MultiFormatterSpec - { - public required int Priority { get; init; } - public required string[] ReportKeys { get; init; } - public MultiValueFormatter ValueFormatter { get; init; } - } - } - - /// - /// The input data to a , - /// containing the currently running application's , - /// and the matched from the Play Report. - /// - public class Value - { - /// - /// 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 XValue properties for that. - ///
- public object BoxedValue => PackedValue.ToObject(); - - #region AsX accessors - - public bool BooleanValue => PackedValue.AsBoolean(); - public byte ByteValye => PackedValue.AsByte(); - public sbyte SByteValye => PackedValue.AsSByte(); - public short ShortValye => PackedValue.AsInt16(); - public ushort UShortValye => PackedValue.AsUInt16(); - public int IntValye => PackedValue.AsInt32(); - public uint UIntValye => PackedValue.AsUInt32(); - public long LongValye => PackedValue.AsInt64(); - public ulong ULongValye => PackedValue.AsUInt64(); - public float FloatValue => PackedValue.AsSingle(); - public double DoubleValue => PackedValue.AsDouble(); - public string StringValue => PackedValue.AsString(); - public Span BinaryValue => PackedValue.AsBinary(); - - #endregion - } - - /// - /// The delegate type that powers single value formatters.
- /// 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 Analyzer.FormattedValue ValueFormatter(Value value); - - /// - /// The delegate type that powers multiple value formatters.
- /// 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 Analyzer.FormattedValue MultiValueFormatter(Value[] value); } diff --git a/src/Ryujinx/Utilities/PlayReport/Delegates.cs b/src/Ryujinx/Utilities/PlayReport/Delegates.cs new file mode 100644 index 000000000..7c8952e18 --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport/Delegates.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace Ryujinx.Ava.Utilities.PlayReport +{ + /// + /// The delegate type that powers single value formatters.
+ /// 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 FormattedValue ValueFormatter(Value value); + + /// + /// The delegate type that powers multiple value formatters.
+ /// Takes in the result values 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 FormattedValue MultiValueFormatter(Value[] value); + + /// + /// The delegate type that powers multiple value formatters. + /// The dictionary passed to this delegate is sparsely populated; + /// that is, not every key specified in the Play Report needs to match for this to be used.
+ /// Takes in the result values 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 FormattedValue SparseMultiValueFormatter(Dictionary values); +} diff --git a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs index 25457744e..ae954c81c 100644 --- a/src/Ryujinx/Utilities/PlayReport/PlayReports.cs +++ b/src/Ryujinx/Utilities/PlayReport/PlayReports.cs @@ -1,6 +1,4 @@ -using static Ryujinx.Ava.Utilities.PlayReport.Analyzer; - -namespace Ryujinx.Ava.Utilities.PlayReport +namespace Ryujinx.Ava.Utilities.PlayReport { public static class PlayReports { @@ -10,7 +8,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport spec => spec .AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode) // reset to normal status when switching between normal & master mode in title screen - .AddValueFormatter("AoCVer", FormattedValue.AlwaysResets) + .AddValueFormatter("AoCVer", FormattedValue.SingleAlwaysResets) ) .AddSpec( "0100f2c0115b6000", diff --git a/src/Ryujinx/Utilities/PlayReport/Specs.cs b/src/Ryujinx/Utilities/PlayReport/Specs.cs new file mode 100644 index 000000000..649813b7a --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport/Specs.cs @@ -0,0 +1,140 @@ +using FluentAvalonia.Core; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Ava.Utilities.PlayReport +{ + /// + /// A mapping of title IDs to value formatter specs. + /// + /// Generally speaking, use the .AddSpec(...) methods instead of creating this class yourself. + /// + public class GameSpec + { + public required string[] TitleIds { get; init; } + public List SimpleValueFormatters { get; } = []; + public List MultiValueFormatters { get; } = []; + public List SparseMultiValueFormatters { 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 GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter) + => AddValueFormatter(SimpleValueFormatters.Count, reportKey, 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 GameSpec AddValueFormatter(int priority, string reportKey, + ValueFormatter valueFormatter) + { + SimpleValueFormatters.Add(new FormatterSpec + { + Priority = priority, ReportKey = reportKey, Formatter = valueFormatter + }); + return this; + } + + /// + /// Add a multi-value formatter to the current + /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs. + /// + /// The key names to match. + /// The function which can format the values. + /// The current , for chaining convenience. + public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter) + => AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter); + + /// + /// Add a multi-value formatter at a specific priority to the current + /// matching a specific set of keys 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 names to match. + /// The function which can format the values. + /// The current , for chaining convenience. + public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys, + MultiValueFormatter valueFormatter) + { + MultiValueFormatters.Add(new MultiFormatterSpec + { + Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter + }); + return this; + } + + /// + /// Add a multi-value formatter to the current + /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs. + ///

+ /// The 'Sparse' multi-value formatters do not require every key to be present. + /// If you need this requirement, use . + ///
+ /// The key names to match. + /// The function which can format the values. + /// The current , for chaining convenience. + public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter) + => AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter); + + /// + /// Add a multi-value formatter at a specific priority to the current + /// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs. + ///

+ /// The 'Sparse' multi-value formatters do not require every key to be present. + /// If you need this requirement, use . + ///
+ /// The resolution priority of this value formatter. Higher resolves sooner. + /// The key names to match. + /// The function which can format the values. + /// The current , for chaining convenience. + public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys, + SparseMultiValueFormatter valueFormatter) + { + SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec + { + Priority = priority, ReportKeys = reportKeys, Formatter = 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 ValueFormatter Formatter { get; init; } + } + + /// + /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values. + /// + public struct MultiFormatterSpec + { + public required int Priority { get; init; } + public required string[] ReportKeys { get; init; } + public MultiValueFormatter Formatter { get; init; } + } + + /// + /// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their sparsely populated potential values. + /// + public struct SparseMultiFormatterSpec + { + public required int Priority { get; init; } + public required string[] ReportKeys { get; init; } + public SparseMultiValueFormatter Formatter { get; init; } + } +} diff --git a/src/Ryujinx/Utilities/PlayReport/Value.cs b/src/Ryujinx/Utilities/PlayReport/Value.cs new file mode 100644 index 000000000..46d47366d --- /dev/null +++ b/src/Ryujinx/Utilities/PlayReport/Value.cs @@ -0,0 +1,130 @@ +using MsgPack; +using Ryujinx.Ava.Utilities.AppLibrary; +using System; + +namespace Ryujinx.Ava.Utilities.PlayReport +{ + /// + /// The input data to a , + /// containing the currently running application's , + /// and the matched from the Play Report. + /// + public class Value + { + /// + /// 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 XValue properties for that. + ///
+ public object BoxedValue => PackedValue.ToObject(); + + public override string ToString() + { + object boxed = BoxedValue; + return boxed == null + ? "null" + : boxed.ToString(); + } + + #region AsX accessors + + public bool BooleanValue => PackedValue.AsBoolean(); + public byte ByteValue => PackedValue.AsByte(); + public sbyte SByteValue => PackedValue.AsSByte(); + public short ShortValue => PackedValue.AsInt16(); + public ushort UShortValue => PackedValue.AsUInt16(); + public int IntValue => PackedValue.AsInt32(); + public uint UIntValue => PackedValue.AsUInt32(); + public long LongValue => PackedValue.AsInt64(); + public ulong ULongValue => PackedValue.AsUInt64(); + public float FloatValue => PackedValue.AsSingle(); + public double DoubleValue => PackedValue.AsDouble(); + public string StringValue => PackedValue.AsString(); + public Span BinaryValue => PackedValue.AsBinary(); + + #endregion + } + + /// + /// 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; } + + /// + /// 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; + + public override string ToString() + { + if (!Handled) + return ""; + + if (Reset) + return ""; + + return FormattedString; + } + + /// + /// 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 ValueFormatter SingleAlwaysResets = _ => ForceReset; + + /// + /// A delegate singleton you can use to always return in a . + /// + public static readonly MultiValueFormatter MultiAlwaysResets = _ => 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 ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue; + } +}