Compare commits
1 Commits
e5fc7fff23
...
Canary-1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c8edaf89e |
@@ -126,14 +126,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
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.Ava.UI.ViewModels.Input;
|
||||
using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Models.Input
|
||||
{
|
||||
public class StickVisualizer : BaseModel, IDisposable
|
||||
{
|
||||
public const int DrawStickPollRate = 50; // Milliseconds per poll.
|
||||
public const int DrawStickCircumference = 5;
|
||||
public const float DrawStickScaleFactor = DrawStickCanvasCenter;
|
||||
public const int DrawStickCanvasSize = 100;
|
||||
public const int DrawStickBorderSize = DrawStickCanvasSize + 5;
|
||||
public const float DrawStickCanvasCenter = (DrawStickCanvasSize - DrawStickCircumference) / 2;
|
||||
public const float MaxVectorLength = DrawStickCanvasSize / 2;
|
||||
|
||||
public CancellationTokenSource PollTokenSource;
|
||||
public CancellationToken PollToken;
|
||||
|
||||
private static float _vectorLength;
|
||||
private static float _vectorMultiplier;
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
private DeviceType _type;
|
||||
public DeviceType Type
|
||||
{
|
||||
get => _type;
|
||||
set
|
||||
{
|
||||
_type = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private GamepadInputConfig _gamepadConfig;
|
||||
public GamepadInputConfig GamepadConfig
|
||||
{
|
||||
get => _gamepadConfig;
|
||||
set
|
||||
{
|
||||
_gamepadConfig = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyboardInputConfig _keyboardConfig;
|
||||
public KeyboardInputConfig KeyboardConfig
|
||||
{
|
||||
get => _keyboardConfig;
|
||||
set
|
||||
{
|
||||
_keyboardConfig = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private (float, float) _uiStickLeft;
|
||||
public (float, float) UiStickLeft
|
||||
{
|
||||
get => (_uiStickLeft.Item1 * DrawStickScaleFactor, _uiStickLeft.Item2 * DrawStickScaleFactor);
|
||||
set
|
||||
{
|
||||
_uiStickLeft = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(UiStickRightX));
|
||||
OnPropertyChanged(nameof(UiStickRightY));
|
||||
OnPropertyChanged(nameof(UiDeadzoneRight));
|
||||
}
|
||||
}
|
||||
|
||||
private (float, float) _uiStickRight;
|
||||
public (float, float) UiStickRight
|
||||
{
|
||||
get => (_uiStickRight.Item1 * DrawStickScaleFactor, _uiStickRight.Item2 * DrawStickScaleFactor);
|
||||
set
|
||||
{
|
||||
_uiStickRight = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(UiStickLeftX));
|
||||
OnPropertyChanged(nameof(UiStickLeftY));
|
||||
OnPropertyChanged(nameof(UiDeadzoneLeft));
|
||||
}
|
||||
}
|
||||
|
||||
public float UiStickLeftX => ClampVector(UiStickLeft).Item1;
|
||||
public float UiStickLeftY => ClampVector(UiStickLeft).Item2;
|
||||
public float UiStickRightX => ClampVector(UiStickRight).Item1;
|
||||
public float UiStickRightY => ClampVector(UiStickRight).Item2;
|
||||
|
||||
public int UiStickCircumference => DrawStickCircumference;
|
||||
public int UiCanvasSize => DrawStickCanvasSize;
|
||||
public int UiStickBorderSize => DrawStickBorderSize;
|
||||
|
||||
public float? UiDeadzoneLeft => _gamepadConfig?.DeadzoneLeft * DrawStickCanvasSize - DrawStickCircumference;
|
||||
public float? UiDeadzoneRight => _gamepadConfig?.DeadzoneRight * DrawStickCanvasSize - DrawStickCircumference;
|
||||
|
||||
private InputViewModel Parent;
|
||||
|
||||
public StickVisualizer(InputViewModel parent)
|
||||
{
|
||||
Parent = parent;
|
||||
|
||||
PollTokenSource = new CancellationTokenSource();
|
||||
PollToken = PollTokenSource.Token;
|
||||
|
||||
Task.Run(Initialize, PollToken);
|
||||
}
|
||||
|
||||
public void UpdateConfig(object config)
|
||||
{
|
||||
if (config is ControllerInputViewModel padConfig)
|
||||
{
|
||||
GamepadConfig = padConfig.Config;
|
||||
Type = DeviceType.Controller;
|
||||
|
||||
return;
|
||||
}
|
||||
else if (config is KeyboardInputViewModel keyConfig)
|
||||
{
|
||||
KeyboardConfig = keyConfig.Config;
|
||||
Type = DeviceType.Keyboard;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Type = DeviceType.None;
|
||||
}
|
||||
|
||||
public async Task Initialize()
|
||||
{
|
||||
(float, float) leftBuffer;
|
||||
(float, float) rightBuffer;
|
||||
|
||||
while (!PollToken.IsCancellationRequested)
|
||||
{
|
||||
leftBuffer = (0f, 0f);
|
||||
rightBuffer = (0f, 0f);
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case DeviceType.Keyboard:
|
||||
IKeyboard keyboard = (IKeyboard)Parent.AvaloniaKeyboardDriver.GetGamepad("0");
|
||||
|
||||
if (keyboard != null)
|
||||
{
|
||||
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
|
||||
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
|
||||
{
|
||||
leftBuffer.Item1 += 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
|
||||
{
|
||||
leftBuffer.Item1 -= 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
|
||||
{
|
||||
leftBuffer.Item2 += 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
|
||||
{
|
||||
leftBuffer.Item2 -= 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
|
||||
{
|
||||
rightBuffer.Item1 += 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
|
||||
{
|
||||
rightBuffer.Item1 -= 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
|
||||
{
|
||||
rightBuffer.Item2 += 1;
|
||||
}
|
||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
|
||||
{
|
||||
rightBuffer.Item2 -= 1;
|
||||
}
|
||||
|
||||
UiStickLeft = leftBuffer;
|
||||
UiStickRight = rightBuffer;
|
||||
}
|
||||
break;
|
||||
|
||||
case DeviceType.Controller:
|
||||
IGamepad controller = Parent.SelectedGamepad;
|
||||
|
||||
if (controller != null)
|
||||
{
|
||||
leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick);
|
||||
rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
|
||||
}
|
||||
break;
|
||||
|
||||
case DeviceType.None:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unable to poll device type \"{Type}\"");
|
||||
}
|
||||
|
||||
UiStickLeft = leftBuffer;
|
||||
UiStickRight = rightBuffer;
|
||||
|
||||
await Task.Delay(DrawStickPollRate, PollToken);
|
||||
}
|
||||
|
||||
PollTokenSource.Dispose();
|
||||
}
|
||||
|
||||
public static (float, float) ClampVector((float, float) vect)
|
||||
{
|
||||
_vectorMultiplier = 1;
|
||||
_vectorLength = MathF.Sqrt((vect.Item1 * vect.Item1) + (vect.Item2 * vect.Item2));
|
||||
|
||||
if (_vectorLength > MaxVectorLength)
|
||||
{
|
||||
_vectorMultiplier = MaxVectorLength / _vectorLength;
|
||||
}
|
||||
|
||||
vect.Item1 = vect.Item1 * _vectorMultiplier + DrawStickCanvasCenter;
|
||||
vect.Item2 = vect.Item2 * _vectorMultiplier + DrawStickCanvasCenter;
|
||||
|
||||
return vect;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
PollTokenSource.Cancel();
|
||||
}
|
||||
|
||||
KeyboardConfig = null;
|
||||
GamepadConfig = null;
|
||||
Parent = null;
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
using Avalonia.Svg.Skia;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models.Input;
|
||||
using Ryujinx.Ava.UI.Views.Input;
|
||||
using Ryujinx.Common.Utilities;
|
||||
@@ -14,30 +10,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
{
|
||||
public partial class ControllerInputViewModel : BaseModel
|
||||
{
|
||||
private GamepadInputConfig _config;
|
||||
public GamepadInputConfig Config
|
||||
{
|
||||
get => _config;
|
||||
set
|
||||
{
|
||||
_config = value;
|
||||
[ObservableProperty] private GamepadInputConfig _config;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private StickVisualizer _visualizer;
|
||||
public StickVisualizer Visualizer
|
||||
{
|
||||
get => _visualizer;
|
||||
set
|
||||
{
|
||||
_visualizer = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isLeft;
|
||||
public bool IsLeft
|
||||
{
|
||||
@@ -63,15 +37,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
}
|
||||
|
||||
public bool HasSides => IsLeft ^ IsRight;
|
||||
|
||||
|
||||
[ObservableProperty] private SvgImage _image;
|
||||
|
||||
|
||||
public InputViewModel ParentModel { get; }
|
||||
|
||||
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config, StickVisualizer visualizer)
|
||||
|
||||
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config)
|
||||
{
|
||||
ParentModel = model;
|
||||
Visualizer = visualizer;
|
||||
model.NotifyChangesEvent += OnParentModelChanged;
|
||||
OnParentModelChanged();
|
||||
config.PropertyChanged += (_, args) =>
|
||||
|
||||
@@ -49,7 +49,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
private int _controller;
|
||||
private string _controllerImage;
|
||||
private int _device;
|
||||
private object _configViewModel;
|
||||
[ObservableProperty] private object _configViewModel;
|
||||
[ObservableProperty] private string _profileName;
|
||||
private bool _isLoaded;
|
||||
|
||||
@@ -74,7 +74,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed));
|
||||
}
|
||||
}
|
||||
public StickVisualizer VisualStick { get; private set; }
|
||||
|
||||
public ObservableCollection<PlayerModel> PlayerIndexes { get; set; }
|
||||
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
|
||||
@@ -95,19 +94,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
public bool IsModified { get; set; }
|
||||
public event Action NotifyChangesEvent;
|
||||
|
||||
public object ConfigViewModel
|
||||
{
|
||||
get => _configViewModel;
|
||||
set
|
||||
{
|
||||
_configViewModel = value;
|
||||
|
||||
VisualStick.UpdateConfig(value);
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerIndex PlayerIdChoose
|
||||
{
|
||||
get => _playerIdChoose;
|
||||
@@ -283,7 +269,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
Devices = [];
|
||||
ProfilesList = [];
|
||||
DeviceList = [];
|
||||
VisualStick = new StickVisualizer(this);
|
||||
|
||||
ControllerImage = ProControllerResource;
|
||||
|
||||
@@ -304,12 +289,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
|
||||
{
|
||||
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick);
|
||||
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig));
|
||||
}
|
||||
|
||||
if (Config is StandardControllerInputConfig controllerInputConfig)
|
||||
{
|
||||
ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig), VisualStick);
|
||||
ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,8 +893,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
_mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates();
|
||||
|
||||
VisualStick.Dispose();
|
||||
|
||||
SelectedGamepad?.Dispose();
|
||||
|
||||
AvaloniaKeyboardDriver.Dispose();
|
||||
|
||||
@@ -6,29 +6,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
{
|
||||
public partial class KeyboardInputViewModel : BaseModel
|
||||
{
|
||||
private KeyboardInputConfig _config;
|
||||
public KeyboardInputConfig Config
|
||||
{
|
||||
get => _config;
|
||||
set
|
||||
{
|
||||
_config = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private StickVisualizer _visualizer;
|
||||
public StickVisualizer Visualizer
|
||||
{
|
||||
get => _visualizer;
|
||||
set
|
||||
{
|
||||
_visualizer = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
[ObservableProperty] private KeyboardInputConfig _config;
|
||||
|
||||
private bool _isLeft;
|
||||
public bool IsLeft
|
||||
@@ -60,10 +38,9 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
public readonly InputViewModel ParentModel;
|
||||
|
||||
public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config, StickVisualizer visualizer)
|
||||
public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config)
|
||||
{
|
||||
ParentModel = model;
|
||||
Visualizer = visualizer;
|
||||
model.NotifyChangesEvent += OnParentModelChanged;
|
||||
OnParentModelChanged();
|
||||
Config = config;
|
||||
|
||||
@@ -316,103 +316,17 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<!-- Controller Picture -->
|
||||
<Image
|
||||
Margin="0,10"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Source="{Binding Image}" />
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Margin="0,10"
|
||||
MinHeight="90">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Image
|
||||
Margin="5,10"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Source="{Binding Image}" />
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Orientation="Horizontal"
|
||||
Spacing="20"
|
||||
HorizontalAlignment="Center">
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Height="{Binding Visualizer.UiStickBorderSize}"
|
||||
Width="{Binding Visualizer.UiStickBorderSize}"
|
||||
IsVisible="{Binding IsLeft}">
|
||||
<Canvas
|
||||
Background="{DynamicResource ThemeBackgroundColor}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}">
|
||||
<Grid
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Background="{DynamicResource ThemeBackgroundColor}">
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Stroke="Black"
|
||||
StrokeThickness="1"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}" />
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Fill="Gray"
|
||||
Opacity="100"
|
||||
Height="{Binding Visualizer.UiDeadzoneLeft}"
|
||||
Width="{Binding Visualizer.UiDeadzoneLeft}" />
|
||||
</Grid>
|
||||
<Ellipse
|
||||
Fill="Red"
|
||||
Width="{Binding Visualizer.UiStickCircumference}"
|
||||
Height="{Binding Visualizer.UiStickCircumference}"
|
||||
Canvas.Bottom="{Binding Visualizer.UiStickLeftY}"
|
||||
Canvas.Left="{Binding Visualizer.UiStickLeftX}" />
|
||||
</Canvas>
|
||||
</Border>
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Height="{Binding Visualizer.UiStickBorderSize}"
|
||||
Width="{Binding Visualizer.UiStickBorderSize}"
|
||||
IsVisible="{Binding IsRight}">
|
||||
<Canvas
|
||||
Background="{DynamicResource ThemeBackgroundColor}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}">
|
||||
<Grid
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Background="{DynamicResource ThemeBackgroundColor}">
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Stroke="Black"
|
||||
StrokeThickness="1"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}" />
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Fill="Gray"
|
||||
Opacity="100"
|
||||
Height="{Binding Visualizer.UiDeadzoneRight}"
|
||||
Width="{Binding Visualizer.UiDeadzoneRight}" />
|
||||
</Grid>
|
||||
<Ellipse
|
||||
Fill="Red"
|
||||
Width="{Binding Visualizer.UiStickCircumference}"
|
||||
Height="{Binding Visualizer.UiStickCircumference}"
|
||||
Canvas.Bottom="{Binding Visualizer.UiStickRightY}"
|
||||
Canvas.Left="{Binding Visualizer.UiStickRightX}" />
|
||||
</Canvas>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5">
|
||||
<StackPanel
|
||||
Margin="8"
|
||||
Orientation="Vertical">
|
||||
@@ -431,8 +345,8 @@
|
||||
Minimum="0"
|
||||
Value="{Binding Config.TriggerThreshold, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Width="25"
|
||||
Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" />
|
||||
Width="25"
|
||||
Text="{Binding Config.TriggerThreshold, StringFormat=\{0:0.00\}}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Orientation="Vertical"
|
||||
|
||||
@@ -309,79 +309,12 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<!-- Controller Picture -->
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
<Image
|
||||
Margin="0,10"
|
||||
MinHeight="90">
|
||||
<StackPanel
|
||||
Margin="10"
|
||||
Orientation="Horizontal"
|
||||
Spacing="20"
|
||||
HorizontalAlignment="Center">
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Height="{Binding Visualizer.UiStickBorderSize}"
|
||||
Width="{Binding Visualizer.UiStickBorderSize}"
|
||||
IsVisible="{Binding IsLeft}">
|
||||
<Canvas
|
||||
Background="{DynamicResource ThemeBackgroundColor}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}">
|
||||
<Grid
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Background="{DynamicResource ThemeBackgroundColor}">
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Stroke="Black"
|
||||
StrokeThickness="1"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"/>
|
||||
</Grid>
|
||||
<Ellipse
|
||||
Fill="Red"
|
||||
Width="{Binding Visualizer.UiStickCircumference}"
|
||||
Height="{Binding Visualizer.UiStickCircumference}"
|
||||
Canvas.Bottom="{Binding Visualizer.UiStickLeftY}"
|
||||
Canvas.Left="{Binding Visualizer.UiStickLeftX}" />
|
||||
</Canvas>
|
||||
</Border>
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="5"
|
||||
Height="{Binding Visualizer.UiStickBorderSize}"
|
||||
Width="{Binding Visualizer.UiStickBorderSize}"
|
||||
IsVisible="{Binding IsRight}">
|
||||
<Canvas
|
||||
Background="{DynamicResource ThemeBackgroundColor}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}">
|
||||
<Grid
|
||||
Height="{Binding Visualizer.UiCanvasSize}"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Background="{DynamicResource ThemeBackgroundColor}">
|
||||
<Ellipse
|
||||
HorizontalAlignment="Center"
|
||||
Stroke="Black"
|
||||
StrokeThickness="1"
|
||||
Width="{Binding Visualizer.UiCanvasSize}"
|
||||
Height="{Binding Visualizer.UiCanvasSize}"/>
|
||||
</Grid>
|
||||
<Ellipse
|
||||
Fill="Red"
|
||||
Width="{Binding Visualizer.UiStickCircumference}"
|
||||
Height="{Binding Visualizer.UiStickCircumference}"
|
||||
Canvas.Bottom="{Binding Visualizer.UiStickRightY}"
|
||||
Canvas.Left="{Binding Visualizer.UiStickRightX}" />
|
||||
</Canvas>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Source="{Binding Image}" />
|
||||
<Border
|
||||
BorderBrush="{DynamicResource ThemeControlBorderColor}"
|
||||
BorderThickness="1"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
|
||||
Width="1100"
|
||||
Height="850"
|
||||
Height="768"
|
||||
MinWidth="800"
|
||||
MinHeight="480"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace Ryujinx.Ava.Utilities.PlayReport
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Runs the configured <see cref="GameSpec.FormatterSpec"/> for the specified game title ID.
|
||||
/// </summary>
|
||||
@@ -98,261 +98,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<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.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<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 FormattedValue.Unhandled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A potential formatted value returned by a <see cref="ValueFormatter"/>.
|
||||
/// </summary>
|
||||
public readonly struct FormattedValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Was any handler able to match anything in the Play Report?
|
||||
/// </summary>
|
||||
public bool Handled { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Did the handler request the caller of the <see cref="Analyzer"/> to reset the existing value?
|
||||
/// </summary>
|
||||
public bool Reset { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
|
||||
/// </summary>
|
||||
public string FormattedString { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// The intended path of execution for having a string to return: simply return the string.
|
||||
/// This implicit conversion will make the struct for you.<br/><br/>
|
||||
///
|
||||
/// If the input is null, <see cref="Unhandled"/> is returned.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The formatted string value.</param>
|
||||
/// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
|
||||
public static implicit operator FormattedValue(string formattedValue)
|
||||
=> formattedValue is not null
|
||||
? new FormattedValue { Handled = true, FormattedString = formattedValue }
|
||||
: Unhandled;
|
||||
|
||||
/// <summary>
|
||||
/// Return this to tell the caller there is no value to return.
|
||||
/// </summary>
|
||||
public static 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 };
|
||||
|
||||
/// <summary>
|
||||
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
|
||||
/// </summary>
|
||||
public static readonly ValueFormatter AlwaysResets = _ => ForceReset;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate factory you can use to always return the specified
|
||||
/// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||
public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A mapping of title IDs to value formatter specs.
|
||||
///
|
||||
/// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
||||
/// </summary>
|
||||
public class GameSpec
|
||||
{
|
||||
public required string[] TitleIds { get; init; }
|
||||
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
|
||||
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Add a value formatter to the current <see cref="GameSpec"/>
|
||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
SimpleValueFormatters.Add(new FormatterSpec
|
||||
{
|
||||
Priority = SimpleValueFormatters.Count, ReportKey = reportKey, ValueFormatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||
/// <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(int priority, string reportKey,
|
||||
ValueFormatter valueFormatter)
|
||||
{
|
||||
SimpleValueFormatters.Add(new FormatterSpec
|
||||
{
|
||||
Priority = priority, ReportKey = reportKey, ValueFormatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <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)
|
||||
{
|
||||
MultiValueFormatters.Add(new MultiFormatterSpec
|
||||
{
|
||||
Priority = SimpleValueFormatters.Count, ReportKeys = reportKeys, ValueFormatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
|
||||
MultiValueFormatter valueFormatter)
|
||||
{
|
||||
MultiValueFormatters.Add(new MultiFormatterSpec
|
||||
{
|
||||
Priority = priority, ReportKeys = reportKeys, ValueFormatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <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 required int Priority { get; init; }
|
||||
public required string ReportKey { get; init; }
|
||||
public ValueFormatter ValueFormatter { get; init; }
|
||||
}
|
||||
|
||||
/// <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 required int Priority { get; init; }
|
||||
public required string[] ReportKeys { get; init; }
|
||||
public MultiValueFormatter ValueFormatter { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The input data to a <see cref="ValueFormatter"/>,
|
||||
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
||||
/// </summary>
|
||||
public class Value
|
||||
{
|
||||
/// <summary>
|
||||
/// The currently running application's <see cref="ApplicationMetadata"/>.
|
||||
/// </summary>
|
||||
public ApplicationMetadata Application { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The matched value from the Play Report.
|
||||
/// </summary>
|
||||
public MessagePackObject PackedValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
|
||||
///
|
||||
/// Does not seem to work well with comparing numeric types,
|
||||
/// so use XValue properties for that.
|
||||
/// </summary>
|
||||
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<byte> BinaryValue => PackedValue.AsBinary();
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type that powers single value formatters.<br/>
|
||||
/// Takes in the result value from the Play Report, and outputs:
|
||||
/// <br/>
|
||||
/// a formatted string,
|
||||
/// <br/>
|
||||
/// a signal that nothing was available to handle it,
|
||||
/// <br/>
|
||||
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||
/// </summary>
|
||||
public delegate Analyzer.FormattedValue ValueFormatter(Value value);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type that powers multiple value formatters.<br/>
|
||||
/// Takes in the result value from the Play Report, and outputs:
|
||||
/// <br/>
|
||||
/// a formatted string,
|
||||
/// <br/>
|
||||
/// a signal that nothing was available to handle it,
|
||||
/// <br/>
|
||||
/// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
|
||||
/// </summary>
|
||||
public delegate Analyzer.FormattedValue MultiValueFormatter(Value[] value);
|
||||
}
|
||||
|
||||
42
src/Ryujinx/Utilities/PlayReport/Delegates.cs
Normal file
42
src/Ryujinx/Utilities/PlayReport/Delegates.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||
{
|
||||
/// <summary>
|
||||
/// The delegate type that powers single value formatters.<br/>
|
||||
/// Takes in the result value from the Play Report, and outputs:
|
||||
/// <br/>
|
||||
/// a formatted string,
|
||||
/// <br/>
|
||||
/// a signal that nothing was available to handle it,
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// The delegate type that powers multiple value formatters.<br/>
|
||||
/// Takes in the result values from the Play Report, and outputs:
|
||||
/// <br/>
|
||||
/// a formatted string,
|
||||
/// <br/>
|
||||
/// a signal that nothing was available to handle it,
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// 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.<br/>
|
||||
/// Takes in the result values from the Play Report, and outputs:
|
||||
/// <br/>
|
||||
/// a formatted string,
|
||||
/// <br/>
|
||||
/// a signal that nothing was available to handle it,
|
||||
/// <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);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
140
src/Ryujinx/Utilities/PlayReport/Specs.cs
Normal file
140
src/Ryujinx/Utilities/PlayReport/Specs.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using FluentAvalonia.Core;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||
{
|
||||
/// <summary>
|
||||
/// A mapping of title IDs to value formatter specs.
|
||||
///
|
||||
/// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
|
||||
/// </summary>
|
||||
public class GameSpec
|
||||
{
|
||||
public required string[] TitleIds { get; init; }
|
||||
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
|
||||
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
|
||||
public List<SparseMultiFormatterSpec> SparseMultiValueFormatters { get; } = [];
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add a value formatter to the current <see cref="GameSpec"/>
|
||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Add a value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||
/// matching a specific key that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||
/// <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(int priority, string reportKey,
|
||||
ValueFormatter valueFormatter)
|
||||
{
|
||||
SimpleValueFormatters.Add(new FormatterSpec
|
||||
{
|
||||
Priority = priority, ReportKey = reportKey, Formatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// </summary>
|
||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||
public GameSpec AddMultiValueFormatter(int priority, string[] reportKeys,
|
||||
MultiValueFormatter valueFormatter)
|
||||
{
|
||||
MultiValueFormatters.Add(new MultiFormatterSpec
|
||||
{
|
||||
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// <br/><br/>
|
||||
/// The 'Sparse' multi-value formatters do not require every key to be present.
|
||||
/// If you need this requirement, use <see cref="AddMultiValueFormatter(string[], Ryujinx.Ava.Utilities.PlayReport.MultiValueFormatter)"/>.
|
||||
/// </summary>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Add a multi-value formatter at a specific priority to the current <see cref="GameSpec"/>
|
||||
/// matching a specific set of keys that could exist in a Play Report for the previously specified title IDs.
|
||||
/// <br/><br/>
|
||||
/// The 'Sparse' multi-value formatters do not require every key to be present.
|
||||
/// If you need this requirement, use <see cref="AddMultiValueFormatter(int, string[], Ryujinx.Ava.Utilities.PlayReport.MultiValueFormatter)"/>.
|
||||
/// </summary>
|
||||
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param>
|
||||
/// <param name="reportKeys">The key names to match.</param>
|
||||
/// <param name="valueFormatter">The function which can format the values.</param>
|
||||
/// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
|
||||
public GameSpec AddSparseMultiValueFormatter(int priority, string[] reportKeys,
|
||||
SparseMultiValueFormatter valueFormatter)
|
||||
{
|
||||
SparseMultiValueFormatters.Add(new SparseMultiFormatterSpec
|
||||
{
|
||||
Priority = priority, ReportKeys = reportKeys, Formatter = valueFormatter
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 required int Priority { get; init; }
|
||||
public required string ReportKey { get; init; }
|
||||
public ValueFormatter Formatter { get; init; }
|
||||
}
|
||||
|
||||
/// <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 required int Priority { get; init; }
|
||||
public required string[] ReportKeys { get; init; }
|
||||
public MultiValueFormatter Formatter { get; init; }
|
||||
}
|
||||
|
||||
/// <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 required int Priority { get; init; }
|
||||
public required string[] ReportKeys { get; init; }
|
||||
public SparseMultiValueFormatter Formatter { get; init; }
|
||||
}
|
||||
}
|
||||
130
src/Ryujinx/Utilities/PlayReport/Value.cs
Normal file
130
src/Ryujinx/Utilities/PlayReport/Value.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using MsgPack;
|
||||
using Ryujinx.Ava.Utilities.AppLibrary;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities.PlayReport
|
||||
{
|
||||
/// <summary>
|
||||
/// The input data to a <see cref="ValueFormatter"/>,
|
||||
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
|
||||
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
|
||||
/// </summary>
|
||||
public class Value
|
||||
{
|
||||
/// <summary>
|
||||
/// The currently running application's <see cref="ApplicationMetadata"/>.
|
||||
/// </summary>
|
||||
public ApplicationMetadata Application { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The matched value from the Play Report.
|
||||
/// </summary>
|
||||
public MessagePackObject PackedValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
|
||||
///
|
||||
/// Does not seem to work well with comparing numeric types,
|
||||
/// so use XValue properties for that.
|
||||
/// </summary>
|
||||
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<byte> BinaryValue => PackedValue.AsBinary();
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A potential formatted value returned by a <see cref="ValueFormatter"/>.
|
||||
/// </summary>
|
||||
public readonly struct FormattedValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Was any handler able to match anything in the Play Report?
|
||||
/// </summary>
|
||||
public bool Handled { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// Did the handler request the caller of the <see cref="Analyzer"/> to reset the existing value?
|
||||
/// </summary>
|
||||
public bool Reset { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// The formatted value, only present if <see cref="Handled"/> is true, and <see cref="Reset"/> is false.
|
||||
/// </summary>
|
||||
public string FormattedString { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// The intended path of execution for having a string to return: simply return the string.
|
||||
/// This implicit conversion will make the struct for you.<br/><br/>
|
||||
///
|
||||
/// If the input is null, <see cref="Unhandled"/> is returned.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The formatted string value.</param>
|
||||
/// <returns>The automatically constructed <see cref="FormattedValue"/> struct.</returns>
|
||||
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 "<Unhandled>";
|
||||
|
||||
if (Reset)
|
||||
return "<Reset>";
|
||||
|
||||
return FormattedString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return this to tell the caller there is no value to return.
|
||||
/// </summary>
|
||||
public static 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 };
|
||||
|
||||
/// <summary>
|
||||
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
|
||||
/// </summary>
|
||||
public static readonly ValueFormatter 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 factory you can use to always return the specified
|
||||
/// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
|
||||
/// </summary>
|
||||
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
|
||||
public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user