Compare commits

...

10 Commits

Author SHA1 Message Date
Vladimir Sokolov
e4e38c4bdd Merge branch 'master' into master 2025-02-08 18:30:35 +10:00
Vova
5f5c76107c Fixed a ban where a custom setting was mistakenly created when starting the game if it did not exist.
Now when starting the game, if a custom setting was created, the current game will be displayed in the settings window.
Code cleanup.
2025-02-08 18:29:31 +10:00
Evan Husted
9c226dcc7a misc: chore: [ci skip] rename ValueFormatter to SingleValueFormatter and some minor cleanups 2025-02-08 01:34:44 -06:00
Evan Husted
30a534edcd misc: chore: [ci skip] generify Formatter Specs to be able to run formatters of different types at interleaving priorities 2025-02-08 01:26:23 -06:00
Evan Husted
1d88771d1b Play Report Analyzer v4
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.
2025-02-08 00:22:34 -06:00
Vova
a92475b8fd Merge branch 'master' of https://github.com/Goodfeat/Ryujinx_alt 2025-02-08 10:51:57 +10:00
Vova
4d5ae23b0b Added game name to user settings window 2025-02-08 10:51:39 +10:00
Vladimir Sokolov
4cb2170ad1 Merge branch 'master' into master 2025-02-08 10:49:55 +10:00
Evan Husted
4e8157688e UI: See what games do/don't have an image & dynamic RPC support in the Game Info popup 2025-02-07 18:34:11 -06:00
Evan Husted
5085af0050 UI: Changed the color of "Ingame" from yellow to orange to stand out better in light mode 2025-02-07 18:28:32 -06:00
23 changed files with 475 additions and 175 deletions

View File

@@ -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<MessagePackObject> PlayReport;
public static event Action<PlayReport> PlayReport;
[field: ThreadStatic]
public static HorizonOptions Options { get; private set; }

View File

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

View File

@@ -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,
}
}

View File

@@ -469,6 +469,8 @@ namespace Ryujinx.Ava
_viewModel.IsGameRunning = true;
Dispatcher.UIThread.InvokeAsync(() =>
{
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowTitleBar);
@@ -582,8 +584,8 @@ namespace Ryujinx.Ava
Rainbow.Disable();
Rainbow.Reset();
//Reload settings when the game is turned off
//(resets custom settings if there were any)
// Reload settings when the game is turned off
// (resets custom settings if there were any)
Program.ReloadConfig();
_isStopped = true;

View File

@@ -28,7 +28,7 @@
Height="28"
HorizontalAlignment="Right"
Content="Addy" />
<Button
<Button
Name="btnRem"
HorizontalAlignment="Right"
Content="Add"
@@ -44,7 +44,6 @@
</Grid>
<ui:NumberBox Value="1" />
<MenuItem
IconSource="Settings"
Header="123 0000"
ToolTip.Tip="What this"/>
</StackPanel>

View File

@@ -3322,6 +3322,31 @@
"zh_TW": "設定"
}
},
{
"ID": "SettingsWithInfo",
"Translations": {
"ar_SA": "{0} - إعدادات",
"de_DE": "Einstellungen - {0}",
"el_GR": "Ρυθμίσεις - {0}",
"en_US": "Settings - {0}",
"es_ES": "Configuración - {0}",
"fr_FR": "Paramètres - {0}",
"he_IL": "{0} - הגדרות",
"it_IT": "Impostazioni - {0}",
"ja_JP": "設定 - {0}",
"ko_KR": "설정 - {0}",
"no_NO": "Innstillinger - {0}",
"pl_PL": "Ustawienia - {0}",
"pt_BR": "Configurações - {0}",
"ru_RU": "Параметры - {0}",
"sv_SE": "Inställningar - {0}",
"th_TH": "ตั้งค่า - {0}",
"tr_TR": "Ayarlar - {0}",
"uk_UA": "Налаштування - {0}",
"zh_CN": "设置 - {0}",
"zh_TW": "設定 - {0}"
}
},
{
"ID": "SettingsTabGeneral",
"Translations": {
@@ -23771,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": ""
}
}
]
}

View File

@@ -10,6 +10,8 @@ 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;
namespace Ryujinx.Ava
@@ -37,6 +39,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
@@ -120,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;

View File

@@ -23,7 +23,6 @@ using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Ryujinx.Ava
{
@@ -157,13 +156,14 @@ namespace Ryujinx.Ava
}
}
public static void ReloadGameConfig(string gamedir)
public static bool FindGameConfig(string gameDir)
{
if (File.Exists(gamedir))
if (File.Exists(gameDir))
{
ConfigurationPath = gamedir;
return true;
}
return false;
}
public static string GetDirGameUserConfig(string gameId, bool rememberGlobalDir = false, bool changeFolderForGame = false)

View File

@@ -37,21 +37,6 @@ namespace Ryujinx.Ava.UI.Controls
AvaloniaXamlLoader.Load(this);
}
public void ToggleUserControl_Click(object sender, RoutedEventArgs args)
{
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return;
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata =>
{
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
});
viewModel.RefreshView();
}
public void ToggleFavorite_Click(object sender, RoutedEventArgs args)
{
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })

View File

@@ -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 @@
</StackPanel>
</Grid>
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<StackPanel Orientation="Vertical" Spacing="5">
<StackPanel Orientation="Horizontal" Spacing="5">
<ui:SymbolIcon Foreground="ForestGreen" Symbol="Checkmark" IsVisible="{Binding AppData.HasRichPresenceAsset}"/>
<TextBlock
Foreground="ForestGreen"
HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasRichPresenceAsset}"
Text="{ext:Locale GameInfoRpcImage}"
TextAlignment="Start"
TextWrapping="Wrap" >
</TextBlock>
<ui:SymbolIcon Foreground="Red" Symbol="Cancel" IsVisible="{Binding !AppData.HasRichPresenceAsset}"/>
<TextBlock
Foreground="Red"
HorizontalAlignment="Stretch"
IsVisible="{Binding !AppData.HasRichPresenceAsset}"
Text="{ext:Locale GameInfoRpcImage}"
TextAlignment="Start"
TextWrapping="Wrap" >
</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<ui:SymbolIcon Foreground="ForestGreen" Symbol="Checkmark" IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"/>
<TextBlock
Foreground="ForestGreen"
HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasDynamicRichPresenceSupport}"
Text="{ext:Locale GameInfoRpcDynamic}"
TextAlignment="Start"
TextWrapping="Wrap" >
</TextBlock>
<ui:SymbolIcon Foreground="Red" Symbol="Cancel" IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"/>
<TextBlock
Foreground="Red"
HorizontalAlignment="Stretch"
IsVisible="{Binding !AppData.HasDynamicRichPresenceSupport}"
Text="{ext:Locale GameInfoRpcDynamic}"
TextAlignment="Start"
TextWrapping="Wrap" >
</TextBlock>
</StackPanel>
</StackPanel>
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<TextBlock
HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasLdnGames}"

View File

@@ -43,19 +43,6 @@ namespace Ryujinx.Ava.UI.Controls
await CompatibilityList.Show((string)playabilityLabel.Tag);
}
public async void EditGameConfiguration_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
{
await new UserConfigWindows(viewModel).ShowDialog((Window)viewModel.TopLevel);
//viewModel.SelectedApplication.UserConfig = File.Exists(Program.GetDirGameUserConfig(viewModel.SelectedApplication.IdString));
viewModel.RefreshView();
}
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel mwvm)

View File

@@ -18,7 +18,7 @@ namespace Ryujinx.Ava.UI.Helpers
LocaleKeys.CompatibilityListNothing or
LocaleKeys.CompatibilityListBoots or
LocaleKeys.CompatibilityListMenus => Brushes.Red,
LocaleKeys.CompatibilityListIngame => Brushes.Yellow,
LocaleKeys.CompatibilityListIngame => Brushes.DarkOrange,
_ => Brushes.ForestGreen
};

View File

@@ -7,6 +7,7 @@ using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using DiscordRPC;
using DynamicData;
using DynamicData.Binding;
using FluentAvalonia.UI.Controls;
@@ -1540,15 +1541,14 @@ namespace Ryujinx.Ava.UI.ViewModels
#if RELEASE
await PerformanceCheck();
#endif
// If a configuration is found in the "/games/xxxxxxxxxxxxxx" folder, the program will load the user setting.
string gameDir = Program.GetDirGameUserConfig(application.IdBaseString, true, true);
if (ConfigurationFileFormat.TryLoad(gameDir, out ConfigurationFileFormat configurationFileFormat))
// If a configuration is found in the "/games/xxxxxxxxxxxxxx" folder, the program will load the user setting.
if (ConfigurationFileFormat.TryLoad(Program.GetDirGameUserConfig(application.IdBaseString), out ConfigurationFileFormat configurationFileFormat))
{
//Program.GetDirGameUserConfig(application.IdBaseString, false);
ConfigurationState.Instance.Load(configurationFileFormat, gameDir, application.IdBaseString);
// Loads the user configuration, having previously changed the global configuration to the user configuration
ConfigurationState.Instance.Load(configurationFileFormat, Program.GetDirGameUserConfig(application.IdBaseString, true, true), application.IdBaseString);
}
Logger.RestartTime();
SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id);

View File

@@ -4,7 +4,6 @@ using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Humanizer;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
@@ -70,11 +69,8 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] private string _ldnServer;
public SettingsHacksViewModel DirtyHacks { get; }
public string GamePath { get; }
public string GameName { get; }
private Bitmap _gameIcon;
private string _gameTitle;
private string _gameId;
public Bitmap GameIcon
@@ -375,7 +371,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool IsInvalidLdnPassphraseVisible { get; set; }
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this()
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(false)
{
_virtualFileSystem = virtualFileSystem;
_contentManager = contentManager;
@@ -388,7 +384,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager, string gamePath, string gameName, string gameId, byte[] gameIconData) : this()
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager, string gamePath, string gameName, string gameId, byte[] gameIconData, bool userDirFind) : this(userDirFind)
{
_virtualFileSystem = virtualFileSystem;
_contentManager = contentManager;
@@ -404,11 +400,14 @@ namespace Ryujinx.Ava.UI.ViewModels
GameTitle = gameName;
GameId = gameId;
string gameDir = Program.GetDirGameUserConfig(gameId,false,true);
if (ConfigurationFileFormat.TryLoad(gameDir, out ConfigurationFileFormat configurationFileFormat))
if (userDirFind)
{
ConfigurationState.Instance.Load(configurationFileFormat, gameDir, gameId);
LoadCurrentConfiguration(); // Needed to load custom configuration
string gameDir = Program.GetDirGameUserConfig(gameId, false, true);
if (ConfigurationFileFormat.TryLoad(gameDir, out ConfigurationFileFormat configurationFileFormat))
{
ConfigurationState.Instance.Load(configurationFileFormat, gameDir, gameId);
LoadCurrentConfiguration(); // Needed to load custom configuration
}
}
if (Program.PreviewerDetached)
@@ -419,7 +418,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public SettingsViewModel()
public SettingsViewModel(bool noLoadGlobalConfig = false)
{
GameDirectories = [];
AutoloadDirectories = [];
@@ -434,7 +433,11 @@ namespace Ryujinx.Ava.UI.ViewModels
if (Program.PreviewerDetached)
{
Task.Run(LoadAvailableGpus);
LoadCurrentConfiguration();
if (!noLoadGlobalConfig)
{
LoadCurrentConfiguration();
}
DirtyHacks = new SettingsHacksViewModel(this);
}
@@ -545,7 +548,8 @@ namespace Ryujinx.Ava.UI.ViewModels
{
ConfigurationState config = ConfigurationState.Instance;
if (string.IsNullOrEmpty(GameId))
//It is necessary that the data is used from the global configuration file
if (string.IsNullOrEmpty(GameId))
{
// User Interface
EnableDiscordIntegration = config.EnableDiscordIntegration;
@@ -568,6 +572,7 @@ namespace Ryujinx.Ava.UI.ViewModels
"Dark" => 2,
_ => 0
};
}
// Input

View File

@@ -134,9 +134,25 @@ namespace Ryujinx.Ava.UI.Views.Main
Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager);
Rainbow.Enable();
await Window.SettingsWindow.ShowDialog(Window);
if (ViewModel.SelectedApplication is null)
{
await Window.SettingsWindow.ShowDialog(Window);
}
else
{
bool userConfigExist = Program.FindGameConfig(Program.GetDirGameUserConfig(ViewModel.SelectedApplication.IdString, false, false));
if (!ViewModel.IsGameRunning || !userConfigExist)
{
await Window.SettingsWindow.ShowDialog(Window);
}
else
{
await new UserConfigWindows(ViewModel, userConfigExist).ShowDialog((Window)ViewModel.TopLevel);
}
}
Rainbow.Disable();
Rainbow.Reset();

View File

@@ -28,9 +28,9 @@ namespace Ryujinx.Ava.UI.Windows
{
internal readonly SettingsViewModel ViewModel;
public UserConfigWindows(MainWindowViewModel viewModel)
public UserConfigWindows(MainWindowViewModel viewModel, bool findUserConfigDir = true)
{
Title = RyujinxApp.FormatTitle(LocaleKeys.Settings);
Title = string.Format(LocaleManager.Instance[LocaleKeys.SettingsWithInfo], viewModel.SelectedApplication.Name, viewModel.SelectedApplication.IdString);
DataContext = ViewModel = new SettingsViewModel(
viewModel.VirtualFileSystem,
@@ -38,7 +38,8 @@ namespace Ryujinx.Ava.UI.Windows
viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.Name,
viewModel.SelectedApplication.IdString,
viewModel.SelectedApplication.Icon);
viewModel.SelectedApplication.Icon,
findUserConfigDir);
ViewModel.CloseWindow += Close;
ViewModel.SaveSettingsEvent += SaveSettings;

View File

@@ -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; }

View File

@@ -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<GameSpec> _specs = [];
public string[] TitleIds => Specs.SelectMany(x => x.TitleIds).ToArray();
public IReadOnlyList<GameSpec> Specs => new ReadOnlyCollection<GameSpec>(_specs);
/// <summary>
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
/// </summary>
@@ -80,7 +85,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <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>
@@ -89,54 +94,21 @@ 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))
return FormattedValue.Unhandled;
foreach (FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
foreach (FormatterSpecBase formatSpec in spec.ValueFormatters.OrderBy(x => x.Priority))
{
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
if (!formatSpec.Format(appMeta, playReport, out FormattedValue value))
continue;
return formatSpec.Formatter(new Value { Application = appMeta, PackedValue = valuePackObject });
}
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))
continue;
packedObjects.Add(valuePackObject);
}
if (packedObjects.Count != formatSpec.ReportKeys.Length)
return FormattedValue.Unhandled;
return formatSpec.Formatter(packedObjects
.Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
.ToArray());
}
foreach (SparseMultiFormatterSpec formatSpec in spec.SparseMultiValueFormatters.OrderBy(x => x.Priority))
{
Dictionary<string, Value> 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 value;
}
return FormattedValue.Unhandled;

View File

@@ -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/>
@@ -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 SingleValueFormatter(SingleValue value);
/// <summary>
/// The delegate type that powers multiple value formatters.<br/>
@@ -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.
@@ -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);
}

View File

@@ -0,0 +1,74 @@
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{
public abstract class MatchedValue<T>
{
protected 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="SingleValueFormatter"/>,
/// 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)
{
}
}
/// <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))
{
}
}
/// <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))
{
}
}
}

View File

@@ -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",

View File

@@ -1,4 +1,7 @@
using FluentAvalonia.Core;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -11,10 +14,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// </summary>
public class GameSpec
{
private int _lastPriority;
public required string[] TitleIds { get; init; }
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
public List<SparseMultiFormatterSpec> SparseMultiValueFormatters { get; } = [];
public List<FormatterSpecBase> ValueFormatters { get; } = [];
/// <summary>
@@ -24,8 +28,8 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="reportKey">The key name to match.</param>
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
=> AddValueFormatter(SimpleValueFormatters.Count, reportKey, valueFormatter);
public GameSpec AddValueFormatter(string reportKey, SingleValueFormatter valueFormatter)
=> AddValueFormatter(_lastPriority++, reportKey, valueFormatter);
/// <summary>
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
@@ -36,11 +40,11 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="valueFormatter">The function which can return a potential formatted value.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddValueFormatter(int priority, string reportKey,
ValueFormatter valueFormatter)
SingleValueFormatter valueFormatter)
{
SimpleValueFormatters.Add(new FormatterSpec
ValueFormatters.Add(new FormatterSpec
{
Priority = priority, ReportKey = reportKey, Formatter = valueFormatter
Priority = priority, ReportKeys = [reportKey], Formatter = valueFormatter
});
return this;
}
@@ -53,7 +57,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddMultiValueFormatter(string[] reportKeys, MultiValueFormatter valueFormatter)
=> AddMultiValueFormatter(MultiValueFormatters.Count, reportKeys, valueFormatter);
=> AddMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
/// <summary>
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
@@ -66,7 +70,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
MultiValueFormatter valueFormatter)
{
MultiValueFormatters.Add(new MultiFormatterSpec
ValueFormatters.Add(new MultiFormatterSpec
{
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
});
@@ -84,7 +88,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <param name="valueFormatter">The function which can format the values.</param>
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public GameSpec AddSparseMultiValueFormatter(string[] reportKeys, SparseMultiValueFormatter valueFormatter)
=> AddSparseMultiValueFormatter(SparseMultiValueFormatters.Count, reportKeys, valueFormatter);
=> AddSparseMultiValueFormatter(_lastPriority++, reportKeys, valueFormatter);
/// <summary>
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
@@ -100,7 +104,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys,
SparseMultiValueFormatter valueFormatter)
{
SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec
ValueFormatters.Add(new SparseMultiFormatterSpec
{
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
});
@@ -111,30 +115,101 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <summary>
/// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
/// </summary>
public struct FormatterSpec
public class FormatterSpec : FormatterSpecBase
{
public required int Priority { get; init; }
public required string ReportKey { get; init; }
public ValueFormatter Formatter { get; init; }
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(ReportKeys[0], out MessagePackObject valuePackObject))
{
result = null;
return false;
}
result = valuePackObject;
return true;
}
}
/// <summary>
/// A struct containing the data for a mapping of an arbitrary key set in a Play Report to a formatter for their potential values.
/// </summary>
public struct MultiFormatterSpec
public class MultiFormatterSpec : FormatterSpecBase
{
public required int Priority { get; init; }
public required string[] ReportKeys { get; init; }
public MultiValueFormatter Formatter { get; init; }
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{
List<MessagePackObject> packedObjects = [];
foreach (var reportKey in ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
{
result = null;
return false;
}
packedObjects.Add(valuePackObject);
}
result = packedObjects;
return true;
}
}
/// <summary>
/// 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.
/// </summary>
public struct SparseMultiFormatterSpec
public class SparseMultiFormatterSpec : FormatterSpecBase
{
public required int Priority { get; init; }
public required string[] ReportKeys { get; init; }
public SparseMultiValueFormatter Formatter { get; init; }
public override bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object result)
{
Dictionary<string, MessagePackObject> packedObjects = [];
foreach (var reportKey in ReportKeys)
{
if (!playReport.ReportData.AsDictionary().TryGetValue(reportKey, out MessagePackObject valuePackObject))
continue;
packedObjects.Add(reportKey, valuePackObject);
}
result = packedObjects;
return true;
}
}
public abstract class FormatterSpecBase
{
public abstract bool GetData(Horizon.Prepo.Types.PlayReport playReport, out object data);
public int Priority { get; init; }
public string[] ReportKeys { get; init; }
public Delegate Formatter { get; init; }
public bool Format(ApplicationMetadata appMeta, Horizon.Prepo.Types.PlayReport playReport, out FormattedValue formattedValue)
{
formattedValue = default;
if (!GetData(playReport, out object data))
return false;
if (data is FormattedValue fv)
{
formattedValue = fv;
return true;
}
switch (Formatter)
{
case SingleValueFormatter svf when data is MessagePackObject mpo:
formattedValue = svf(new SingleValue(mpo) { Application = appMeta, PlayReport = playReport });
return true;
case MultiValueFormatter mvf when data is List<MessagePackObject> messagePackObjects:
formattedValue = mvf(new MultiValue(messagePackObjects) { Application = appMeta, PlayReport = playReport });
return true;
case SparseMultiValueFormatter smvf when
data is Dictionary<string, MessagePackObject> sparseMessagePackObjects:
formattedValue = smvf(new SparseMultiValue(sparseMessagePackObjects) { Application = appMeta, PlayReport = playReport });
return true;
default:
throw new InvalidOperationException("Formatter delegate is not of a known type!");
}
}
}
}

View File

@@ -1,20 +1,20 @@
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.PlayReport
{
/// <summary>
/// The input data to a <see cref="ValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// The base input data to a ValueFormatter delegate,
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
/// </summary>
public class Value
public readonly struct Value
{
/// <summary>
/// The currently running application's <see cref="ApplicationMetadata"/>.
/// </summary>
public ApplicationMetadata Application { get; init; }
public Value(MessagePackObject packedValue)
{
PackedValue = packedValue;
}
/// <summary>
/// The matched value from the Play Report.
@@ -37,6 +37,17 @@ namespace Ryujinx.Ava.Utilities.PlayReport
: boxed.ToString();
}
public static implicit operator Value(MessagePackObject matched) => new(matched);
public static Value[] ConvertPackedObjects(IEnumerable<MessagePackObject> packObjects)
=> packObjects.Select(packObject => new Value(packObject)).ToArray();
public static Dictionary<string, Value> ConvertPackedObjectMap(Dictionary<string, MessagePackObject> packObjects)
=> packObjects.ToDictionary(
x => x.Key,
x => new Value(x.Value)
);
#region AsX accessors
public bool BooleanValue => PackedValue.AsBoolean();
@@ -57,7 +68,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
}
/// <summary>
/// A potential formatted value returned by a <see cref="ValueFormatter"/>.
/// A potential formatted value returned by a ValueFormatter delegate.
/// </summary>
public readonly struct FormattedValue
{
@@ -103,28 +114,47 @@ namespace Ryujinx.Ava.Utilities.PlayReport
/// <summary>
/// Return this to tell the caller there is no value to return.
/// </summary>
public static FormattedValue Unhandled => default;
public static readonly FormattedValue Unhandled = default;
/// <summary>
/// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> for.
/// </summary>
public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
public static readonly FormattedValue ForceReset = new() { Handled = true, Reset = true };
/// <summary>
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="SingleValueFormatter"/>.
/// </summary>
public static readonly ValueFormatter SingleAlwaysResets = _ => ForceReset;
public static readonly SingleValueFormatter SingleAlwaysResets = _ => ForceReset;
/// <summary>
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="MultiValueFormatter"/>.
/// </summary>
public static readonly MultiValueFormatter MultiAlwaysResets = _ => ForceReset;
/// <summary>
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="SparseMultiValueFormatter"/>.
/// </summary>
public static readonly SparseMultiValueFormatter SparseMultiAlwaysResets = _ => ForceReset;
/// <summary>
/// A delegate factory you can use to always return the specified
/// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
/// <paramref name="formattedValue"/> in a <see cref="SingleValueFormatter"/>.
/// </summary>
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
public static SingleValueFormatter SingleAlwaysReturns(string formattedValue) => _ => formattedValue;
/// <summary>
/// A delegate factory you can use to always return the specified
/// <paramref name="formattedValue"/> in a <see cref="MultiValueFormatter"/>.
/// </summary>
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
public static MultiValueFormatter MultiAlwaysReturns(string formattedValue) => _ => formattedValue;
/// <summary>
/// A delegate factory you can use to always return the specified
/// <paramref name="formattedValue"/> in a <see cref="SparseMultiValueFormatter"/>.
/// </summary>
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
public static SparseMultiValueFormatter SparseMultiAlwaysReturns(string formattedValue) => _ => formattedValue;
}
}