Compare commits

...

7 Commits

Author SHA1 Message Date
Frog Business 67bbe6b628 Merge f73ffd1fd0 into d1da937fce 2025-02-06 09:17:10 +01:00
Evan Husted d1da937fce misc: chore: [ci skip] XMLdocs on new Play Report Analyzer members 2025-02-05 19:51:43 -06:00
Evan Husted 4a8f98126f [ci skip] remove test 2025-02-05 19:45:29 -06:00
Evan Husted e55629a908 misc: chore: [ci skip] Play Report Analyzer: Added Multi Value formatters 2025-02-05 19:42:36 -06:00
Evan Husted c638a7daf8 misc: chore: Move Play Report analyzer into a dedicated namespace and remove the PlayReport name prefix on types 2025-02-05 19:27:44 -06:00
Piplup 5e5e180fea PlayReportAnalyzer: Added Pokemon Scarlet and Violet (#630)
Every base game location excluding buildings are done, DLC locations
will be added at a later point
2025-02-05 18:32:27 -06:00
Barış Hamil f73ffd1fd0 Ability to assign hotkeys to cycle controllers for players 2025-02-01 12:30:51 +03:00
13 changed files with 517 additions and 212 deletions
@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Ryujinx.Common.Configuration.Hid namespace Ryujinx.Common.Configuration.Hid
{ {
public class KeyboardHotkeys public class KeyboardHotkeys
@@ -13,5 +15,6 @@ namespace Ryujinx.Common.Configuration.Hid
public Key VolumeDown { get; set; } public Key VolumeDown { get; set; }
public Key CustomVSyncIntervalIncrement { get; set; } public Key CustomVSyncIntervalIncrement { get; set; }
public Key CustomVSyncIntervalDecrement { get; set; } public Key CustomVSyncIntervalDecrement { get; set; }
public List<Key> CycleControllers { get; set; }
} }
} }
+23
View File
@@ -40,6 +40,7 @@ using Ryujinx.HLE;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input; using Ryujinx.Input;
using Ryujinx.Input.HLE; using Ryujinx.Input.HLE;
@@ -49,6 +50,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -1308,6 +1310,18 @@ namespace Ryujinx.Ava
_viewModel.Volume = Device.GetVolume(); _viewModel.Volume = Device.GetVolume();
break; break;
case KeyboardHotkeyState.CycleControllersPlayer1:
case KeyboardHotkeyState.CycleControllersPlayer2:
case KeyboardHotkeyState.CycleControllersPlayer3:
case KeyboardHotkeyState.CycleControllersPlayer4:
case KeyboardHotkeyState.CycleControllersPlayer5:
case KeyboardHotkeyState.CycleControllersPlayer6:
case KeyboardHotkeyState.CycleControllersPlayer7:
case KeyboardHotkeyState.CycleControllersPlayer8:
var player = currentHotkeyState - KeyboardHotkeyState.CycleControllersPlayer1;
var ivm = new UI.ViewModels.Input.InputViewModel();
Dispatcher.UIThread.Invoke(() => ivm.CyclePlayerDevice(player));
break;
case KeyboardHotkeyState.None: case KeyboardHotkeyState.None:
(_keyboardInterface as AvaloniaKeyboard).Clear(); (_keyboardInterface as AvaloniaKeyboard).Clear();
break; break;
@@ -1390,6 +1404,15 @@ namespace Ryujinx.Ava
state = KeyboardHotkeyState.CustomVSyncIntervalDecrement; state = KeyboardHotkeyState.CustomVSyncIntervalDecrement;
} }
foreach (var cycle in ConfigurationState.Instance.Hid.Hotkeys.Value.CycleControllers?.Select((value, index) => (value, index)) ?? [])
{
if (_keyboardInterface.IsPressed((Key)cycle.value))
{
state = KeyboardHotkeyState.CycleControllersPlayer1 + cycle.index;
break;
}
}
return state; return state;
} }
} }
+26 -1
View File
@@ -5672,6 +5672,31 @@
"zh_TW": "啟用警告日誌" "zh_TW": "啟用警告日誌"
} }
}, },
{
"ID": "SettingsTabHotkeysCycleControllers",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Cycle Controllers",
"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": "SettingsTabLoggingEnableErrorLogs", "ID": "SettingsTabLoggingEnableErrorLogs",
"Translations": { "Translations": {
@@ -23698,4 +23723,4 @@
} }
} }
] ]
} }
@@ -14,5 +14,13 @@ namespace Ryujinx.Ava.Common
VolumeDown, VolumeDown,
CustomVSyncIntervalIncrement, CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement, CustomVSyncIntervalDecrement,
CycleControllersPlayer1,
CycleControllersPlayer2,
CycleControllersPlayer3,
CycleControllersPlayer4,
CycleControllersPlayer5,
CycleControllersPlayer6,
CycleControllersPlayer7,
CycleControllersPlayer8
} }
} }
+3 -7
View File
@@ -4,16 +4,12 @@ using MsgPack;
using Ryujinx.Ava.Utilities; using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary; using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration; using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.PlayReport;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE; using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon; using Ryujinx.Horizon;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text; using System.Text;
namespace Ryujinx.Ava namespace Ryujinx.Ava
@@ -130,8 +126,8 @@ namespace Ryujinx.Ava
if (!TitleIDs.CurrentApplication.Value.HasValue) return; if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) return; if (_discordPresencePlaying is null) return;
PlayReportAnalyzer.FormattedValue formattedValue = Analyzer.FormattedValue formattedValue =
PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport); PlayReports.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
if (!formattedValue.Handled) return; if (!formattedValue.Handled) return;
@@ -1,6 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace Ryujinx.Ava.UI.Models.Input namespace Ryujinx.Ava.UI.Models.Input
{ {
@@ -28,8 +33,15 @@ namespace Ryujinx.Ava.UI.Models.Input
[ObservableProperty] private Key _customVSyncIntervalDecrement; [ObservableProperty] private Key _customVSyncIntervalDecrement;
public ObservableCollection<CycleController> CycleControllers { get; set; } = new ObservableCollection<CycleController>();
public ICommand AddCycleController { get; set; }
public ICommand RemoveCycleController { get; set; }
public bool CanRemoveCycleController => CycleControllers.Count > 0 && CycleControllers.Count < 8;
public HotkeyConfig(KeyboardHotkeys config) public HotkeyConfig(KeyboardHotkeys config)
{ {
AddCycleController = MiniCommand.Create(() => CycleControllers.Add(new CycleController(CycleControllers.Count + 1, Key.Unbound)));
RemoveCycleController = MiniCommand.Create(() => CycleControllers.Remove(CycleControllers.Last()));
if (config == null) if (config == null)
return; return;
@@ -44,6 +56,7 @@ namespace Ryujinx.Ava.UI.Models.Input
VolumeDown = config.VolumeDown; VolumeDown = config.VolumeDown;
CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement; CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement;
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement; CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
CycleControllers.AddRange((config.CycleControllers ?? []).Select((x, i) => new CycleController(i + 1, x)));
} }
public KeyboardHotkeys GetConfig() => public KeyboardHotkeys GetConfig() =>
@@ -60,6 +73,7 @@ namespace Ryujinx.Ava.UI.Models.Input
VolumeDown = VolumeDown, VolumeDown = VolumeDown,
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement, CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement, CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
CycleControllers = CycleControllers.Select(x => x.Hotkey).ToList()
}; };
} }
} }
@@ -0,0 +1,48 @@
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.Ava.UI.ViewModels
{
public class CycleController : BaseModel
{
private string _player;
private Key _hotkey;
public string Player
{
get => _player;
set
{
_player = value;
OnPropertyChanged(nameof(Player));
}
}
public Key Hotkey
{
get => _hotkey;
set
{
_hotkey = value;
OnPropertyChanged(nameof(Hotkey));
}
}
public CycleController(int v, Key x)
{
Player = v switch
{
1 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer1],
2 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer2],
3 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer3],
4 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer4],
5 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer5],
6 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer6],
7 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer7],
8 => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer8],
_ => LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer] + " " + v
};
Hotkey = x;
}
}
}
@@ -897,5 +897,13 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
AvaloniaKeyboardDriver.Dispose(); AvaloniaKeyboardDriver.Dispose();
} }
public void CyclePlayerDevice(int player)
{
LoadDevices();
PlayerId = (PlayerIndex)player;
Device = (Device + 1) % Devices.Count;
Save();
}
} }
} }
@@ -1,4 +1,4 @@
<UserControl <UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsHotkeysView" x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsHotkeysView"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -15,17 +15,18 @@
<viewModels:SettingsViewModel /> <viewModels:SettingsViewModel />
</Design.DataContext> </Design.DataContext>
<UserControl.Styles> <UserControl.Styles>
<Style Selector="StackPanel > StackPanel"> <Style Selector="StackPanel StackPanel">
<Setter Property="Margin" Value="10, 0, 0, 0" /> <Setter Property="Margin" Value="10, 0, 0, 0" />
<Setter Property="Orientation" Value="Horizontal" /> <Setter Property="Orientation" Value="Horizontal" />
</Style> </Style>
<Style Selector="StackPanel > StackPanel > TextBlock"> <Style Selector="StackPanel StackPanel > TextBlock">
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="230" /> <Setter Property="Width" Value="230" />
</Style> </Style>
<Style Selector="ToggleButton"> <Style Selector="ToggleButton, Button">
<Setter Property="Width" Value="90" /> <Setter Property="Width" Value="90" />
<Setter Property="Height" Value="27" /> <Setter Property="Height" Value="27" />
<Setter Property="Padding" Value="0,5,0,5" /> <!-- Added vertical padding -->
</Style> </Style>
<Style Selector="ToggleButton > TextBlock"> <Style Selector="ToggleButton > TextBlock">
<Setter Property="TextAlignment" Value="Center" /> <Setter Property="TextAlignment" Value="Center" />
@@ -39,79 +40,123 @@
VerticalScrollBarVisibility="Auto"> VerticalScrollBarVisibility="Auto">
<Border Classes="settings"> <Border Classes="settings">
<StackPanel <StackPanel
Name="SettingButtons"
Margin="10" Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical" Orientation="Vertical"
Spacing="10"> Spacing="10"
Name="SettingButtons">
<TextBlock <TextBlock
Classes="h1" Classes="h1"
Text="{ext:Locale SettingsTabHotkeysHotkeys}" /> Text="{ext:Locale SettingsTabHotkeysHotkeys}" />
<StackPanel> <StackPanel
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleVSyncModeHotkey}" /> Margin="10,0,0,0"
<ToggleButton Name="ToggleVSyncMode"> Spacing="10"
<TextBlock Text="{Binding KeyboardHotkey.ToggleVSyncMode, Converter={x:Static helpers:KeyValueConverter.Instance}}" /> Orientation="Vertical">
</ToggleButton> <StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleVSyncModeHotkey}" />
<ToggleButton Name="ToggleVSyncMode">
<TextBlock Text="{Binding KeyboardHotkey.ToggleVSyncMode, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysScreenshotHotkey}" />
<ToggleButton Name="Screenshot">
<TextBlock Text="{Binding KeyboardHotkey.Screenshot, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysShowUiHotkey}" />
<ToggleButton Name="ShowUI">
<TextBlock Text="{Binding KeyboardHotkey.ShowUI, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysPauseHotkey}" />
<ToggleButton Name="Pause">
<TextBlock Text="{Binding KeyboardHotkey.Pause, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleMuteHotkey}" />
<ToggleButton Name="ToggleMute">
<TextBlock Text="{Binding KeyboardHotkey.ToggleMute, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleUpHotkey}" />
<ToggleButton Name="ResScaleUp">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleDownHotkey}" />
<ToggleButton Name="ResScaleDown">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeUpHotkey}" />
<ToggleButton Name="VolumeUp">
<TextBlock Text="{Binding KeyboardHotkey.VolumeUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeDownHotkey}" />
<ToggleButton Name="VolumeDown">
<TextBlock Text="{Binding KeyboardHotkey.VolumeDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalIncrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalIncrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalDecrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalDecrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
</StackPanel> </StackPanel>
<StackPanel> <Separator Height="1" />
<TextBlock Text="{ext:Locale SettingsTabHotkeysScreenshotHotkey}" /> <StackPanel Margin="0">
<ToggleButton Name="Screenshot"> <TextBlock
<TextBlock Text="{Binding KeyboardHotkey.Screenshot, Converter={x:Static helpers:KeyValueConverter.Instance}}" /> Classes="h1"
</ToggleButton> Text="{ext:Locale SettingsTabHotkeysCycleControllers}" />
</StackPanel> <StackPanel Orientation="Horizontal" Spacing="10">
<StackPanel> <Button
<TextBlock Text="{ext:Locale SettingsTabHotkeysShowUiHotkey}" /> Content="{ext:Locale SettingsTabGeneralAdd}"
<ToggleButton Name="ShowUI"> Margin="10,0,0,0"
<TextBlock Text="{Binding KeyboardHotkey.ShowUI, Converter={x:Static helpers:KeyValueConverter.Instance}}" /> Command="{Binding KeyboardHotkey.AddCycleController}" />
</ToggleButton> <Button
</StackPanel> Content="{ext:Locale SettingsTabGeneralRemove}"
<StackPanel> IsEnabled="{Binding KeyboardHotkey.CanRemoveCycleController}"
<TextBlock Text="{ext:Locale SettingsTabHotkeysPauseHotkey}" /> Command="{Binding KeyboardHotkey.RemoveCycleController}" />
<ToggleButton Name="Pause"> </StackPanel>
<TextBlock Text="{Binding KeyboardHotkey.Pause, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysToggleMuteHotkey}" />
<ToggleButton Name="ToggleMute">
<TextBlock Text="{Binding KeyboardHotkey.ToggleMute, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleUpHotkey}" />
<ToggleButton Name="ResScaleUp">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysResScaleDownHotkey}" />
<ToggleButton Name="ResScaleDown">
<TextBlock Text="{Binding KeyboardHotkey.ResScaleDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeUpHotkey}" />
<ToggleButton Name="VolumeUp">
<TextBlock Text="{Binding KeyboardHotkey.VolumeUp, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel>
<TextBlock Text="{ext:Locale SettingsTabHotkeysVolumeDownHotkey}" />
<ToggleButton Name="VolumeDown">
<TextBlock Text="{Binding KeyboardHotkey.VolumeDown, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalIncrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalIncrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
<TextBlock Text="{ext:Locale SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey}" />
<ToggleButton Name="CustomVSyncIntervalDecrement">
<TextBlock Text="{Binding KeyboardHotkey.CustomVSyncIntervalDecrement, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel> </StackPanel>
<ItemsControl ItemsSource="{Binding KeyboardHotkey.CycleControllers}"
Name="CycleControllers">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
Margin="10,0,0,0"
Orientation="Vertical"
Spacing="10" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Player}" />
<ToggleButton>
<TextBlock
Text="{Binding Hotkey, Converter={x:Static helpers:KeyValueConverter.Instance}}" />
</ToggleButton>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel> </StackPanel>
</Border> </Border>
</ScrollViewer> </ScrollViewer>
@@ -3,11 +3,14 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using DynamicData;
using Ryujinx.Ava.Input; using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Input; using Ryujinx.Input;
using Ryujinx.Input.Assigner; using Ryujinx.Input.Assigner;
using System.Linq;
using Button = Ryujinx.Input.Button; using Button = Ryujinx.Input.Button;
using Key = Ryujinx.Common.Configuration.Hid.Key; using Key = Ryujinx.Common.Configuration.Hid.Key;
@@ -21,16 +24,21 @@ namespace Ryujinx.Ava.UI.Views.Settings
public SettingsHotkeysView() public SettingsHotkeysView()
{ {
InitializeComponent(); InitializeComponent();
RegisterEvents();
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
CycleControllers.LayoutUpdated += (_, _1) => RegisterEvents();
}
private void RegisterEvents()
{
foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) foreach (ILogical visual in SettingButtons.GetLogicalDescendants())
{ {
if (visual is ToggleButton button and not CheckBox) if (visual is ToggleButton button and not CheckBox)
{ {
button.IsCheckedChanged -= Button_IsCheckedChanged;
button.IsCheckedChanged += Button_IsCheckedChanged; button.IsCheckedChanged += Button_IsCheckedChanged;
} }
} }
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
} }
protected override void OnPointerReleased(PointerReleasedEventArgs e) protected override void OnPointerReleased(PointerReleasedEventArgs e)
@@ -116,6 +124,13 @@ namespace Ryujinx.Ava.UI.Views.Settings
case "CustomVSyncIntervalDecrement": case "CustomVSyncIntervalDecrement":
viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = buttonValue.AsHidType<Key>(); viewModel.KeyboardHotkey.CustomVSyncIntervalDecrement = buttonValue.AsHidType<Key>();
break; break;
default:
var index = button.FindAncestorOfType<ItemsControl>().GetLogicalDescendants().OfType<ToggleButton>().IndexOf(button);
if (index >= 0 && viewModel.KeyboardHotkey.CycleControllers != null)
{
viewModel.KeyboardHotkey.CycleControllers[index].Hotkey = buttonValue.AsHidType<Key>();
}
break;
} }
} }
}; };
-85
View File
@@ -1,85 +0,0 @@
using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
namespace Ryujinx.Ava.Utilities
{
public static class PlayReport
{
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
.AddSpec(
"01007ef00011e000",
spec => spec
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
// reset to normal status when switching between normal & master mode in title screen
.AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets)
)
.AddSpec(
"0100f2c0115b6000",
spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec(
"010075000ecbe000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec(
"010028600ebda000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Global & China IDs
["0100152000022000", "010075100e8ec000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
);
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) =>
value.DoubleValue switch
{
> 800d => "Exploring the Sky Islands",
< -201d => "Exploring the Depths",
_ => "Roaming Hyrule"
};
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value)
=> value.StringValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
// CC Classes
"50cc" => "50cc",
"100cc" => "100cc",
"150cc" => "150cc",
"Mirror" => "Mirror (150cc)",
"200cc" => "200cc",
// Modes
"GrandPrix" => "Grand Prix",
"TimeAttack" => "Time Trials",
"VS" => "VS Races",
"Battle" => "Battle Mode",
"RaceStart" => "Selecting a Course",
"Race" => "Racing",
_ => PlayReportFormattedValue.ForceReset
};
}
}
@@ -6,27 +6,27 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
namespace Ryujinx.Ava.Utilities namespace Ryujinx.Ava.Utilities.PlayReport
{ {
/// <summary> /// <summary>
/// The entrypoint for the Play Report analysis system. /// The entrypoint for the Play Report analysis system.
/// </summary> /// </summary>
public class PlayReportAnalyzer public class Analyzer
{ {
private readonly List<PlayReportGameSpec> _specs = []; private readonly List<GameSpec> _specs = [];
/// <summary> /// <summary>
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration. /// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
/// </summary> /// </summary>
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param> /// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
/// <param name="transform">The configuration function for the analysis spec.</param> /// <param name="transform">The configuration function for the analysis spec.</param>
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns> /// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform) public Analyzer AddSpec(string titleId, Func<GameSpec, GameSpec> transform)
{ {
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _), Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}."); $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] })); _specs.Add(transform(new GameSpec { TitleIds = [titleId] }));
return this; return this;
} }
@@ -35,13 +35,13 @@ namespace Ryujinx.Ava.Utilities
/// </summary> /// </summary>
/// <param name="titleId">The ID of the game to listen to Play Reports in.</param> /// <param name="titleId">The ID of the game to listen to Play Reports in.</param>
/// <param name="transform">The configuration function for the analysis spec.</param> /// <param name="transform">The configuration function for the analysis spec.</param>
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns> /// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform) public Analyzer AddSpec(string titleId, Action<GameSpec> transform)
{ {
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _), Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}."); $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform)); _specs.Add(new GameSpec { TitleIds = [titleId] }.Apply(transform));
return this; return this;
} }
@@ -50,15 +50,15 @@ namespace Ryujinx.Ava.Utilities
/// </summary> /// </summary>
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param> /// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
/// <param name="transform">The configuration function for the analysis spec.</param> /// <param name="transform">The configuration function for the analysis spec.</param>
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns> /// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, public Analyzer AddSpec(IEnumerable<string> titleIds,
Func<PlayReportGameSpec, PlayReportGameSpec> transform) Func<GameSpec, GameSpec> transform)
{ {
string[] tids = titleIds.ToArray(); string[] tids = titleIds.ToArray();
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)), Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}."); $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] })); _specs.Add(transform(new GameSpec { TitleIds = [..tids] }));
return this; return this;
} }
@@ -67,20 +67,20 @@ namespace Ryujinx.Ava.Utilities
/// </summary> /// </summary>
/// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param> /// <param name="titleIds">The IDs of the games to listen to Play Reports in.</param>
/// <param name="transform">The configuration function for the analysis spec.</param> /// <param name="transform">The configuration function for the analysis spec.</param>
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns> /// <returns>The current <see cref="Analyzer"/>, for chaining convenience.</returns>
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform) public Analyzer AddSpec(IEnumerable<string> titleIds, Action<GameSpec> transform)
{ {
string[] tids = titleIds.ToArray(); string[] tids = titleIds.ToArray();
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)), Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}."); $"Cannot use a non-hexadecimal string as the Title ID for a {nameof(GameSpec)}.");
_specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform)); _specs.Add(new GameSpec { TitleIds = [..tids] }.Apply(transform));
return this; return this;
} }
/// <summary> /// <summary>
/// Runs the configured <see cref="PlayReportGameSpec.FormatterSpec"/> for the specified game title ID. /// Runs the configured <see cref="GameSpec.FormatterSpec"/> for the specified game title ID.
/// </summary> /// </summary>
/// <param name="runningGameId">The game currently running.</param> /// <param name="runningGameId">The game currently running.</param>
/// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param> /// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
@@ -95,25 +95,44 @@ namespace Ryujinx.Ava.Utilities
if (!playReport.IsDictionary) if (!playReport.IsDictionary)
return FormattedValue.Unhandled; return FormattedValue.Unhandled;
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec)) if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out GameSpec spec))
return FormattedValue.Unhandled; return FormattedValue.Unhandled;
foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority)) foreach (GameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
{ {
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject)) if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
continue; continue;
return formatSpec.ValueFormatter(new PlayReportValue return formatSpec.ValueFormatter(new Value
{ {
Application = appMeta, PackedValue = valuePackObject Application = appMeta, PackedValue = valuePackObject
}); });
} }
foreach (GameSpec.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
.Select(packObject => new Value { Application = appMeta, PackedValue = packObject })
.ToArray());
}
return FormattedValue.Unhandled; return FormattedValue.Unhandled;
} }
/// <summary> /// <summary>
/// A potential formatted value returned by a <see cref="PlayReportValueFormatter"/>. /// A potential formatted value returned by a <see cref="ValueFormatter"/>.
/// </summary> /// </summary>
public readonly struct FormattedValue public readonly struct FormattedValue
{ {
@@ -123,7 +142,7 @@ namespace Ryujinx.Ava.Utilities
public bool Handled { get; private init; } public bool Handled { get; private init; }
/// <summary> /// <summary>
/// Did the handler request the caller of the <see cref="PlayReportAnalyzer"/> to reset the existing value? /// Did the handler request the caller of the <see cref="Analyzer"/> to reset the existing value?
/// </summary> /// </summary>
public bool Reset { get; private init; } public bool Reset { get; private init; }
@@ -151,42 +170,43 @@ namespace Ryujinx.Ava.Utilities
public static FormattedValue Unhandled => default; public static FormattedValue Unhandled => default;
/// <summary> /// <summary>
/// Return this to suggest the caller reset the value it's using the <see cref="PlayReportAnalyzer"/> for. /// Return this to suggest the caller reset the value it's using the <see cref="Analyzer"/> for.
/// </summary> /// </summary>
public static FormattedValue ForceReset => new() { Handled = true, Reset = true }; public static FormattedValue ForceReset => new() { Handled = true, Reset = true };
/// <summary> /// <summary>
/// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="PlayReportValueFormatter"/>. /// A delegate singleton you can use to always return <see cref="ForceReset"/> in a <see cref="ValueFormatter"/>.
/// </summary> /// </summary>
public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset; public static readonly ValueFormatter AlwaysResets = _ => ForceReset;
/// <summary> /// <summary>
/// A delegate factory you can use to always return the specified /// A delegate factory you can use to always return the specified
/// <paramref name="formattedValue"/> in a <see cref="PlayReportValueFormatter"/>. /// <paramref name="formattedValue"/> in a <see cref="ValueFormatter"/>.
/// </summary> /// </summary>
/// <param name="formattedValue">The string to always return for this delegate instance.</param> /// <param name="formattedValue">The string to always return for this delegate instance.</param>
public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue; public static ValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
} }
} }
/// <summary> /// <summary>
/// A mapping of title IDs to value formatter specs. /// A mapping of title IDs to value formatter specs.
/// ///
/// <remarks>Generally speaking, use the <see cref="PlayReportAnalyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks> /// <remarks>Generally speaking, use the <see cref="Analyzer"/>.AddSpec(...) methods instead of creating this class yourself.</remarks>
/// </summary> /// </summary>
public class PlayReportGameSpec public class GameSpec
{ {
public required string[] TitleIds { get; init; } public required string[] TitleIds { get; init; }
public List<FormatterSpec> SimpleValueFormatters { get; } = []; public List<FormatterSpec> SimpleValueFormatters { get; } = [];
public List<MultiFormatterSpec> MultiValueFormatters { get; } = [];
/// <summary> /// <summary>
/// Add a value formatter to the current <see cref="PlayReportGameSpec"/> /// 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. /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
/// </summary> /// </summary>
/// <param name="reportKey">The key name to match.</param> /// <param name="reportKey">The key name to match.</param>
/// <param name="valueFormatter">The function which can return a potential formatted value.</param> /// <param name="valueFormatter">The function which can return a potential formatted value.</param>
/// <returns>The current <see cref="PlayReportGameSpec"/>, for chaining convenience.</returns> /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter) public GameSpec AddValueFormatter(string reportKey, ValueFormatter valueFormatter)
{ {
SimpleValueFormatters.Add(new FormatterSpec SimpleValueFormatters.Add(new FormatterSpec
{ {
@@ -196,15 +216,15 @@ namespace Ryujinx.Ava.Utilities
} }
/// <summary> /// <summary>
/// Add a value formatter at a specific priority to the current <see cref="PlayReportGameSpec"/> /// 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. /// matching a specific key that could exist in a Play Report for the previously specified title IDs.
/// </summary> /// </summary>
/// <param name="priority">The resolution priority of this value formatter. Higher resolves sooner.</param> /// <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="reportKey">The key name to match.</param>
/// <param name="valueFormatter">The function which can return a potential formatted value.</param> /// <param name="valueFormatter">The function which can return a potential formatted value.</param>
/// <returns>The current <see cref="PlayReportGameSpec"/>, for chaining convenience.</returns> /// <returns>The current <see cref="GameSpec"/>, for chaining convenience.</returns>
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, public GameSpec AddValueFormatter(int priority, string reportKey,
PlayReportValueFormatter valueFormatter) ValueFormatter valueFormatter)
{ {
SimpleValueFormatters.Add(new FormatterSpec SimpleValueFormatters.Add(new FormatterSpec
{ {
@@ -212,6 +232,40 @@ namespace Ryujinx.Ava.Utilities
}); });
return this; 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> /// <summary>
/// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value. /// A struct containing the data for a mapping of a key in a Play Report to a formatter for its potential value.
@@ -220,16 +274,26 @@ namespace Ryujinx.Ava.Utilities
{ {
public required int Priority { get; init; } public required int Priority { get; init; }
public required string ReportKey { get; init; } public required string ReportKey { get; init; }
public PlayReportValueFormatter ValueFormatter { 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> /// <summary>
/// The input data to a <see cref="PlayReportValueFormatter"/>, /// The input data to a <see cref="ValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>, /// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/> from the Play Report. /// and the matched <see cref="MessagePackObject"/> from the Play Report.
/// </summary> /// </summary>
public class PlayReportValue public class Value
{ {
/// <summary> /// <summary>
/// The currently running application's <see cref="ApplicationMetadata"/>. /// The currently running application's <see cref="ApplicationMetadata"/>.
@@ -245,7 +309,7 @@ namespace Ryujinx.Ava.Utilities
/// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/> /// Access the <see cref="PackedValue"/> as its underlying .NET type.<br/>
/// ///
/// Does not seem to work well with comparing numeric types, /// Does not seem to work well with comparing numeric types,
/// so use <see cref="PackedValue"/> and the AsX (where X is a numerical type name i.e. Int32) methods for that. /// so use XValue properties for that.
/// </summary> /// </summary>
public object BoxedValue => PackedValue.ToObject(); public object BoxedValue => PackedValue.ToObject();
@@ -269,14 +333,26 @@ namespace Ryujinx.Ava.Utilities
} }
/// <summary> /// <summary>
/// The delegate type that powers the entire analysis system (as it currently is).<br/> /// The delegate type that powers single value formatters.<br/>
/// Takes in the result value from the Play Report, and outputs: /// Takes in the result value from the Play Report, and outputs:
/// <br/> /// <br/>
/// a formatted string, /// a formatted string,
/// <br/> /// <br/>
/// a signal that nothing was available to handle it, /// a signal that nothing was available to handle it,
/// <br/> /// <br/>
/// OR a signal to reset the value that the caller is using the <see cref="PlayReportAnalyzer"/> for. /// OR a signal to reset the value that the caller is using the <see cref="Analyzer"/> for.
/// </summary> /// </summary>
public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value); 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);
} }
@@ -0,0 +1,129 @@
using static Ryujinx.Ava.Utilities.PlayReport.Analyzer;
namespace Ryujinx.Ava.Utilities.PlayReport
{
public static class PlayReports
{
public static Analyzer Analyzer { get; } = new Analyzer()
.AddSpec(
"01007ef00011e000",
spec => spec
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
// reset to normal status when switching between normal & master mode in title screen
.AddValueFormatter("AoCVer", FormattedValue.AlwaysResets)
)
.AddSpec(
"0100f2c0115b6000",
spec => spec
.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec(
"010075000ecbe000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec(
"010028600ebda000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Global & China IDs
["0100152000022000", "010075100e8ec000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
)
.AddSpec(
["0100a3d008c5c000", "01008f6008c5e000"],
spec => spec
.AddValueFormatter("area_no", PokemonSVArea)
.AddValueFormatter("team_circle", PokemonSVUnionCircle)
);
private static FormattedValue BreathOfTheWild_MasterMode(Value value)
=> value.BoxedValue is 1 ? "Playing Master Mode" : FormattedValue.ForceReset;
private static FormattedValue TearsOfTheKingdom_CurrentField(Value value) =>
value.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 SuperMarioOdysseyChina_AssistMode(Value value)
=> value.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 MarioKart8Deluxe_Mode(Value value)
=> value.StringValue switch
{
// Single Player
"Single" => "Single Player",
// Multiplayer
"Multi-2players" => "Multiplayer 2 Players",
"Multi-3players" => "Multiplayer 3 Players",
"Multi-4players" => "Multiplayer 4 Players",
// Wireless/LAN Play
"Local-Single" => "Wireless/LAN Play",
"Local-2players" => "Wireless/LAN Play 2 Players",
// CC Classes
"50cc" => "50cc",
"100cc" => "100cc",
"150cc" => "150cc",
"Mirror" => "Mirror (150cc)",
"200cc" => "200cc",
// Modes
"GrandPrix" => "Grand Prix",
"TimeAttack" => "Time Trials",
"VS" => "VS Races",
"Battle" => "Battle Mode",
"RaceStart" => "Selecting a Course",
"Race" => "Racing",
_ => FormattedValue.ForceReset
};
private static FormattedValue PokemonSVUnionCircle(Value value)
=> value.BoxedValue is 0 ? "Playing Alone" : "Playing in a group";
private static FormattedValue PokemonSVArea(Value value)
=> value.StringValue switch
{
// Base Game Locations
"a_w01" => "South Area One",
"a_w02" => "Mesagoza",
"a_w03" => "The Pokemon League",
"a_w04" => "South Area Two",
"a_w05" => "South Area Four",
"a_w06" => "South Area Six",
"a_w07" => "South Area Five",
"a_w08" => "South Area Three",
"a_w09" => "West Area One",
"a_w10" => "Asado Desert",
"a_w11" => "West Area Two",
"a_w12" => "Medali",
"a_w13" => "Tagtree Thicket",
"a_w14" => "East Area Three",
"a_w15" => "Artazon",
"a_w16" => "East Area Two",
"a_w18" => "Casseroya Lake",
"a_w19" => "Glaseado Mountain",
"a_w20" => "North Area Three",
"a_w21" => "North Area One",
"a_w22" => "North Area Two",
"a_w23" => "The Great Crater of Paldea",
"a_w24" => "South Paldean Sea",
"a_w25" => "West Paldean Sea",
"a_w26" => "East Paldean Sea",
"a_w27" => "Nouth Paldean Sea",
//TODO DLC Locations
_ => FormattedValue.ForceReset
};
}
}