Compare commits

..

11 Commits

21 changed files with 408 additions and 908 deletions

View File

@@ -13,14 +13,12 @@ namespace Ryujinx.Common
public const string BuildGitHash = "%%RYUJINX_BUILD_GIT_HASH%%";
private const string ReleaseChannelName = "%%RYUJINX_TARGET_RELEASE_CHANNEL_NAME%%";
private const string ConfigFileName = "%%RYUJINX_CONFIG_FILE_NAME%%";
private const string ConfigFileNameOverride = "%%RYUJINX_CONFIG_FILE_NAME_OVERRIDE%%";
public const string ReleaseChannelOwner = "%%RYUJINX_TARGET_RELEASE_CHANNEL_OWNER%%";
public const string ReleaseChannelSourceRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_SOURCE_REPO%%";
public const string ReleaseChannelRepo = "%%RYUJINX_TARGET_RELEASE_CHANNEL_REPO%%";
public static string ConfigName => !ConfigFileName.StartsWith("%%") ? ConfigFileName : "Config.json";
public static string CustomConfigNameOverride => !ConfigFileNameOverride.StartsWith("%%") ? ConfigFileNameOverride : "CustomConfigOverride.json";
public static bool IsValid =>
!BuildGitHash.StartsWith("%%") &&
@@ -28,8 +26,7 @@ namespace Ryujinx.Common
!ReleaseChannelOwner.StartsWith("%%") &&
!ReleaseChannelSourceRepo.StartsWith("%%") &&
!ReleaseChannelRepo.StartsWith("%%") &&
!ConfigFileName.StartsWith("%%") &&
!ConfigFileNameOverride.StartsWith("%%");
!ConfigFileName.StartsWith("%%");
public static bool IsCanaryBuild => IsValid && ReleaseChannelName.Equals(CanaryChannel);

View File

@@ -47,7 +47,8 @@ namespace Ryujinx.Common
"01006f8002326000", // Animal Crossings: New Horizons
"01009bf0072d4000", // Captain Toad: Treasure Tracker
"01009510001ca000", // Fast RMX
"01005CA01580E000", // Persona 5 Royale
"01005CA01580E000", // Persona 5 Royal
"0100b880154fc000", // Persona 5 The Royal (Japan)
"010015100b514000", // Super Mario Bros. Wonder
"0100000000010000", // Super Mario Odyssey

View File

@@ -2,35 +2,36 @@ using MsgPack;
using Ryujinx.Horizon.Common;
using Ryujinx.Memory;
using System;
using System.Threading;
namespace Ryujinx.Horizon
{
public static class HorizonStatic
{
internal static void HandlePlayReport(MessagePackObject report) => PlayReportPrinted?.Invoke(report);
internal static void HandlePlayReport(MessagePackObject report) =>
new Thread(() => PlayReport?.Invoke(report))
{
Name = "HLE.PlayReportEvent",
IsBackground = true,
Priority = ThreadPriority.AboveNormal
}.Start();
public static event Action<MessagePackObject> PlayReportPrinted;
[ThreadStatic]
private static HorizonOptions _options;
public static event Action<MessagePackObject> PlayReport;
[ThreadStatic]
private static ISyscallApi _syscall;
[field: ThreadStatic]
public static HorizonOptions Options { get; private set; }
[ThreadStatic]
private static IVirtualMemoryManager _addressSpace;
[field: ThreadStatic]
public static ISyscallApi Syscall { get; private set; }
[ThreadStatic]
private static IThreadContext _threadContext;
[field: ThreadStatic]
public static IVirtualMemoryManager AddressSpace { get; private set; }
[ThreadStatic]
private static int _threadHandle;
[field: ThreadStatic]
public static IThreadContext ThreadContext { get; private set; }
public static HorizonOptions Options => _options;
public static ISyscallApi Syscall => _syscall;
public static IVirtualMemoryManager AddressSpace => _addressSpace;
public static IThreadContext ThreadContext => _threadContext;
public static int CurrentThreadHandle => _threadHandle;
[field: ThreadStatic]
public static int CurrentThreadHandle { get; private set; }
public static void Register(
HorizonOptions options,
@@ -39,11 +40,11 @@ namespace Ryujinx.Horizon
IThreadContext threadContext,
int threadHandle)
{
_options = options;
_syscall = syscallApi;
_addressSpace = addressSpace;
_threadContext = threadContext;
_threadHandle = threadHandle;
Options = options;
Syscall = syscallApi;
AddressSpace = addressSpace;
ThreadContext = threadContext;
CurrentThreadHandle = threadHandle;
}
}
}

View File

@@ -1,3 +1,4 @@
using Gommon;
using MsgPack;
using MsgPack.Serialization;
using Ryujinx.Common.Logging;
@@ -11,6 +12,7 @@ using Ryujinx.Horizon.Sdk.Sf;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using System;
using System.Text;
using System.Threading;
using ApplicationId = Ryujinx.Horizon.Sdk.Ncm.ApplicationId;
namespace Ryujinx.Horizon.Prepo.Ipc

View File

@@ -23069,7 +23069,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "可游玩",
"zh_TW": "可暢順遊玩 (Playable)"
"zh_TW": "可暢順遊玩"
}
},
{
@@ -23094,7 +23094,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "进入游戏",
"zh_TW": "大致可遊玩 (Ingame)"
"zh_TW": "大致可遊玩"
}
},
{
@@ -23119,7 +23119,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "菜单",
"zh_TW": "只開啟至遊戲開始功能表 (Menus)"
"zh_TW": "只開啟至遊戲開始功能表"
}
},
{
@@ -23144,7 +23144,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "启动",
"zh_TW": "只能啟動 (Boots)"
"zh_TW": "只能啟動"
}
},
{
@@ -23169,7 +23169,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "什么都没有",
"zh_TW": "無法啟動 (Nothing)"
"zh_TW": "無法啟動"
}
},
{

View File

@@ -56,7 +56,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
HorizonStatic.PlayReportPrinted += HandlePlayReport;
HorizonStatic.PlayReport += HandlePlayReport;
}
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
@@ -135,21 +135,21 @@ namespace Ryujinx.Ava
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) return;
PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
if (!value.Handled) return;
if (value.Reset)
{
_discordPresencePlaying.Details = $"Playing {_currentApp.Title}";
Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter.");
}
else
{
_discordPresencePlaying.Details = value.FormattedString;
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
}
UpdatePlayingState();
PlayReport.Analyzer.FormatPlayReportValue(TitleIDs.CurrentApplication.Value, _currentApp, playReport)
.Match(out bool handled,
() =>
{
_discordPresencePlaying.Details = $"Playing {_currentApp.Title}";
Logger.Info?.Print(LogClass.UI, "Reset Discord RPC based on a supported play report value formatter.");
},
formattedString =>
{
_discordPresencePlaying.Details = formattedString;
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
});
if (handled)
UpdatePlayingState();
}
private static string TruncateToByteLength(string input)

View File

@@ -160,28 +160,6 @@ namespace Ryujinx.Ava
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
string overrideLocalConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.CustomConfigNameOverride);
string overrideAppDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.CustomConfigNameOverride);
// Copies and reloads the configuration file if the game was loaded with arguments
// based on global configuration
if (CommandLineState.CountArguments > 0)
{
if (File.Exists(localConfigurationPath))
{
File.Copy(localConfigurationPath, overrideLocalConfigurationPath, overwrite: true);
}
localConfigurationPath = overrideLocalConfigurationPath;
if (File.Exists(appDataConfigurationPath))
{
File.Copy(appDataConfigurationPath, overrideAppDataConfigurationPath, overwrite: true);
}
appDataConfigurationPath = overrideAppDataConfigurationPath;
}
// Now load the configuration as the other subsystems are now registered
if (File.Exists(localConfigurationPath))
{
@@ -254,35 +232,8 @@ namespace Ryujinx.Ava
_ => ConfigurationState.Instance.HideCursor,
};
// Check if memoryManagerMode was overridden.
if (CommandLineState.OverrideMemoryManagerMode is not null)
if (Enum.TryParse(CommandLineState.OverrideMemoryManagerMode, true, out MemoryManagerMode result))
{
ConfigurationState.Instance.System.MemoryManagerMode.Value = result;
}
// Check if PPTC was overridden.
if (CommandLineState.OverridePPTC is not null)
if (Enum.TryParse(CommandLineState.OverridePPTC, true, out bool result))
{
ConfigurationState.Instance.System.EnablePtc.Value = result;
}
// Check if region was overridden.
if (CommandLineState.OverrideSystemRegion is not null)
if (Enum.TryParse(CommandLineState.OverrideSystemRegion, true, out Ryujinx.HLE.HOS.SystemState.RegionCode result))
{
ConfigurationState.Instance.System.Region.Value = (Utilities.Configuration.System.Region)result;
}
//Check if language was overridden.
if (CommandLineState.OverrideSystemLanguage is not null)
if (Enum.TryParse(CommandLineState.OverrideSystemLanguage, true, out Ryujinx.HLE.HOS.SystemState.SystemLanguage result))
{
ConfigurationState.Instance.System.Language.Value = (Utilities.Configuration.System.Language)result;
}
// Check if hardware-acceleration was overridden. MemoryManagerMode ( outdated! )
// Check if hardware-acceleration was overridden.
if (CommandLineState.OverrideHardwareAcceleration != null)
UseHardwareAcceleration = CommandLineState.OverrideHardwareAcceleration.Value;
}

View File

@@ -375,11 +375,15 @@ namespace Ryujinx.Ava.UI.Controls
png.SaveTo(fileStream);
}
public async void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args)
public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await new ArgumentsConfigWindows(viewModel).ShowDialog((Window)viewModel.TopLevel);
ShortcutHelper.CreateAppShortcut(
viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.Name,
viewModel.SelectedApplication.IdString,
viewModel.SelectedApplication.Icon
);
}
public async void RunApplication_Click(object sender, RoutedEventArgs args)

View File

@@ -86,6 +86,13 @@
Text="{Binding Version}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
IsVisible="{Binding HasPlayabilityInfo}"
HorizontalAlignment="Stretch"
Text="{Binding LocalizedStatus}"
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel>
</Border>
<StackPanel

View File

@@ -1,10 +1,8 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
@@ -28,7 +26,6 @@ using Ryujinx.HLE.HOS.Services.Time.TimeZone;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
@@ -66,56 +63,12 @@ namespace Ryujinx.Ava.UI.ViewModels
public event Action CloseWindow;
public event Action SaveSettingsEvent;
public event Action CompareSettingsEvent;
private int _networkInterfaceIndex;
private int _multiplayerModeIndex;
private string _ldnPassphrase;
[ObservableProperty] private string _ldnServer;
public SettingsHacksViewModel DirtyHacks { get; }
public string GamePath { get; }
public string GameName { get; }
private Bitmap _gameIcon;
private string _gameTitle;
private string _gameId;
public Bitmap GameIcon
{
get => _gameIcon;
set
{
if (_gameIcon != value)
{
_gameIcon = value;
}
}
}
public string GameTitle
{
get => _gameTitle;
set
{
if (_gameTitle != value)
{
_gameTitle = value;
}
}
}
public string GameId
{
get => _gameId;
set
{
if (_gameId != value)
{
_gameId = value;
}
}
}
public int ResolutionScale
{
@@ -391,30 +344,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager, string gamePath, string gameName, string gameId, byte[] gameIconData) : this()
{
_virtualFileSystem = virtualFileSystem;
_contentManager = contentManager;
if (gameIconData != null && gameIconData.Length > 0)
{
using (var ms = new MemoryStream(gameIconData))
{
GameIcon = new Bitmap(ms);
}
}
GameTitle = gameName;
GameId = gameId;
if (Program.PreviewerDetached)
{
Task.Run(LoadTimeZones);
DirtyHacks = new SettingsHacksViewModel(this);
}
}
public SettingsViewModel()
{
GameDirectories = [];
@@ -787,11 +716,6 @@ namespace Ryujinx.Ava.UI.ViewModels
SaveSettings();
}
public void CreateShortcut()
{
CompareSettingsEvent?.Invoke(); //raises an event to create a shortcut with arguments
}
public void OkButton()
{
SaveSettings();

View File

@@ -1,236 +0,0 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsApplyOverride"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d"
x:DataType="viewModels:SettingsViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<ScrollViewer
Name="AllSettings"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border Classes="settings">
<StackPanel
Margin="10"
HorizontalAlignment="Stretch"
Orientation="Vertical"
Spacing="10">
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabSystemCore}" />
<StackPanel
Margin="10,0,0,0"
Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{ext:Locale SettingsTabSystemSystemRegion}" Width="250" />
<ComboBox
SelectedIndex="{Binding Region}"
ToolTip.Tip="{ext:Locale RegionTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemRegionJapan}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemRegionUSA}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemRegionEurope}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemRegionAustralia}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemRegionChina}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemRegionKorea}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemRegionTaiwan}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel
Margin="10,0,0,0"
Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
Text="{ext:Locale SettingsTabSystemSystemLanguage}"
ToolTip.Tip="{ext:Locale LanguageTooltip}"
Width="250" />
<ComboBox
SelectedIndex="{Binding Language}"
ToolTip.Tip="{ext:Locale LanguageTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageJapanese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageAmericanEnglish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageFrench}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageGerman}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageItalian}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageSpanish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageChinese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageKorean}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageDutch}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguagePortuguese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageRussian}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageTaiwanese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageBritishEnglish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageCanadianFrench}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageLatinAmericanSpanish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageSimplifiedChinese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageTraditionalChinese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageBrazilianPortuguese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageSwedish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageNorwegian}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabCpu}" />
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<CheckBox IsChecked="{Binding EnablePptc}">
<TextBlock Text="{ext:Locale SettingsTabSystemEnablePptc}"
ToolTip.Tip="{ext:Locale PptcToggleTooltip}" />
</CheckBox>
</StackPanel>
<StackPanel
Margin="10,0,0,0"
HorizontalAlignment="Stretch"
Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
Text="{ext:Locale SettingsTabSystemMemoryManagerMode}"
ToolTip.Tip="{ext:Locale MemoryManagerTooltip}"
Width="250" />
<ComboBox SelectedIndex="{Binding MemoryMode}"
ToolTip.Tip="{ext:Locale MemoryManagerTooltip}"
HorizontalContentAlignment="Left"
Width="350">
<ComboBoxItem
ToolTip.Tip="{ext:Locale MemoryManagerSoftwareTooltip}">
<TextBlock
Text="{ext:Locale SettingsTabSystemMemoryManagerModeSoftware}" />
</ComboBoxItem>
<ComboBoxItem
ToolTip.Tip="{ext:Locale MemoryManagerHostTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemMemoryManagerModeHost}" />
</ComboBoxItem>
<ComboBoxItem
ToolTip.Tip="{ext:Locale MemoryManagerUnsafeTooltip}">
<TextBlock
Text="{ext:Locale SettingsTabSystemMemoryManagerModeHostUnchecked}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<CheckBox IsChecked="{Binding UseHypervisor}"
IsVisible="{Binding IsAppleSiliconMac}"
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}">
<TextBlock Text="{ext:Locale SettingsTabSystemUseHypervisor}"
ToolTip.Tip="{ext:Locale UseHypervisorTooltip}" />
</CheckBox>
</StackPanel>
<Separator Height="1" />
<TextBlock Classes="h1" Text="{ext:Locale SettingsTabGraphics}" />
<StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10">
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale SettingsTabGraphicsBackendTooltip}"
Text="{ext:Locale SettingsTabGraphicsBackend}"
Width="250" />
<ComboBox
Name="GraphicsBackendSelector"
Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{ext:Locale SettingsTabGraphicsBackendTooltip}"
SelectedIndex="{Binding GraphicsBackendIndex}">
<ComboBoxItem IsVisible="{Binding IsVulkanAvailable}" ToolTip.Tip="{ext:Locale SettingsTabGraphicsBackendAutoTooltip}" >
<TextBlock Text="{ext:Locale SettingsTabGraphicsBackendAuto}" />
</ComboBoxItem>
<ComboBoxItem IsVisible="{Binding IsVulkanAvailable}">
<TextBlock Text="Vulkan" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsOpenGLAvailable}">
<TextBlock Text="OpenGL" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsAppleSiliconMac}">
<TextBlock Text="Metal (ARM Mac only, Experimental)" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale GraphicsBackendThreadingTooltip}"
Text="{ext:Locale SettingsTabGraphicsBackendMultithreading}"
Width="250" />
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{ext:Locale GalThreadingTooltip}"
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
<ComboBoxItem>
<TextBlock Text="{ext:Locale CommonAuto}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale CommonOff}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale CommonOn}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>

View File

@@ -1,12 +0,0 @@
using Avalonia.Controls;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsApplyOverride : UserControl
{
public SettingsApplyOverride()
{
InitializeComponent();
}
}
}

View File

@@ -1,104 +0,0 @@
<window:StyleableAppWindow
x:Class="Ryujinx.Ava.UI.Windows.ArgumentsConfigWindows"
xmlns="https://github.com/avaloniaui"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
Width="1100"
Height="768"
MinWidth="800"
MinHeight="480"
WindowStartupLocation="CenterOwner"
x:DataType="viewModels:SettingsViewModel"
mc:Ignorable="d"
Focusable="True">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinWidth="600" RowDefinitions="Auto,*,Auto">
<ContentPresenter
x:Name="ContentPresenter"
Grid.Row="1"
IsVisible="False"
KeyboardNavigation.IsTabStop="False"/>
<Grid Name="Pages" IsVisible="False" Grid.Row="2">
<settings:SettingsApplyOverride Name="AllSettings" />
</Grid>
<ui:NavigationView
Grid.Row="1"
IsSettingsVisible="False"
Name="NavPanel"
IsBackEnabled="False"
PaneDisplayMode="Left"
Margin="2,10,10,0"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
OpenPaneLength="200"
IsPaneToggleButtonVisible="False">
<!-- For image -->
<ui:NavigationView.PaneHeader>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding GameId}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,0,0,10"
TextAlignment="Center" Grid.Row="0" />
<Image Source="{Binding GameIcon}"
Width="160"
Height="160"
Grid.Row="1"
Margin="0,0,0,10"
HorizontalAlignment="Center" />
<TextBlock Text="{Binding GameTitle}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,0,0,10"
TextAlignment="Center" Grid.Row="2" />
<Separator Height="1" Grid.Row="3" Margin="0,0,0,10" HorizontalAlignment="Stretch"/>
</Grid>
</ui:NavigationView.PaneHeader>
<ui:NavigationView.MenuItems>
<ui:NavigationViewItem
Margin="2,10,10,0"
Content="{ext:Locale Settings}"
Tag="AllSettings">
<ui:NavigationViewItem.IconSource>
<ui:FontIconSource
FontFamily="avares://Ryujinx/Assets/Fonts#Segoe Fluent Icons"
Glyph="{helpers:GlyphValueConverter Chip}" />
</ui:NavigationViewItem.IconSource>
</ui:NavigationViewItem>
</ui:NavigationView.MenuItems>
</ui:NavigationView>
<ReversibleStackPanel
Grid.Row="2"
Margin="10"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{Binding IsMacOS}">
<Button
Content="{ext:Locale GameListContextMenuCreateShortcut}"
Command="{Binding CreateShortcut}" />
<Button
HotKey="Escape"
Content="{ext:Locale ControllerSettingsClose}"
Command="{Binding CancelButton}" />
</ReversibleStackPanel>
</Grid>
</window:StyleableAppWindow>

View File

@@ -1,162 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using Projektanker.Icons.Avalonia;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Configuration;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.Input;
using System;
using System.IO;
using System.Linq;
using Key = Avalonia.Input.Key;
namespace Ryujinx.Ava.UI.Windows
{
public partial class ArgumentsConfigWindows : StyleableAppWindow
{
internal readonly SettingsViewModel ViewModel;
public string GamePath { get; }
public string GameName { get; }
public string GameId { get; }
public byte[] GameIconData { get; }
public static int OverrideBackendThreading { get; private set; }
public static int OverrideGraphicsBackend { get; private set; }
public static int OverrideSystemLanguage { get; private set; }
public static int OverrideSystemRegion { get; private set; }
public static bool OverridePPTC { get; private set; }
public static int OverrideMemoryManagerMode { get; private set; }
public ArgumentsConfigWindows(MainWindowViewModel viewModel)
{
Title = RyujinxApp.FormatTitle(LocaleKeys.Settings);
DataContext = ViewModel = new SettingsViewModel(
viewModel.VirtualFileSystem,
viewModel.ContentManager,
viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.Name,
viewModel.SelectedApplication.IdString,
viewModel.SelectedApplication.Icon);
GamePath = viewModel.SelectedApplication.Path;
GameName = viewModel.SelectedApplication.Name;
GameId = viewModel.SelectedApplication.IdString;
GameIconData = viewModel.SelectedApplication.Icon;
OverrideBackendThreading = ViewModel.GraphicsBackendMultithreadingIndex;
OverrideGraphicsBackend = ViewModel.GraphicsBackendIndex;
OverrideSystemLanguage = ViewModel.Language;
OverrideSystemRegion = ViewModel.Region;
OverridePPTC = ViewModel.EnablePptc;
OverrideMemoryManagerMode = ViewModel.MemoryMode;
ViewModel.CloseWindow += Close;
ViewModel.CompareSettingsEvent += CompareConfiguration;
InitializeComponent();
Load();
#if DEBUG
this.AttachDevTools(new KeyGesture(Key.F12, KeyModifiers.Alt));
#endif
}
public void CompareConfiguration()
{
ShortcutHelper.CreateAppShortcut(
GamePath,
GameName,
GameId,
GameIconData,
GetArguments()
);
}
private string GetArguments()
{
string line = "";
if (OverrideBackendThreading != ViewModel.GraphicsBackendMultithreadingIndex)
{
string _result = Enum.GetName(typeof(BackendThreading), ViewModel.GraphicsBackendMultithreadingIndex);
line += " --backend-threading " + _result;
}
if (OverrideGraphicsBackend != ViewModel.GraphicsBackendIndex)
{
string _result = Enum.GetName(typeof(GraphicsBackend), ViewModel.GraphicsBackendIndex);
line += " -g " + _result;
}
if (OverridePPTC != ViewModel.EnablePptc)
{
string _result = ViewModel.EnablePptc.ToString();
line += " --pptc " + _result;
}
if (OverrideMemoryManagerMode != ViewModel.MemoryMode)
{
string _result = Enum.GetName(typeof(MemoryManagerMode), ViewModel.MemoryMode);
line += " -m " + _result;
}
if (OverrideSystemRegion != ViewModel.Region)
{
string _result = Enum.GetName(typeof(RegionCode), ViewModel.Region);
line += " --system-region " + _result;
}
if (OverrideSystemLanguage != ViewModel.Language)
{
string _result = Enum.GetName(typeof(SystemLanguage), ViewModel.Language);
line += " --system-language " + _result;
}
return line;
}
private void Load()
{
Pages.Children.Clear();
NavPanel.SelectionChanged += NavPanelOnSelectionChanged;
NavPanel.SelectedItem = NavPanel.MenuItems.ElementAt(0);
}
private void NavPanelOnSelectionChanged(object sender, NavigationViewSelectionChangedEventArgs e)
{
if (e.SelectedItem is NavigationViewItem navItem && navItem.Tag is not null)
{
switch (navItem.Tag.ToString())
{
case nameof(AllSettings):
NavPanel.Content = AllSettings;
break;
default:
throw new NotImplementedException();
}
}
}
protected override void OnClosing(WindowClosingEventArgs e)
{
base.OnClosing(e);
}
}
}

View File

@@ -7,6 +7,8 @@ using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.Compat;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.Loaders.Processes.Extensions;
@@ -21,9 +23,30 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public bool Favorite { get; set; }
public byte[] Icon { get; set; }
public string Name { get; set; } = "Unknown";
public ulong Id { get; set; }
private ulong _id;
public ulong Id
{
get => _id;
set
{
_id = value;
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
}
}
public string Developer { get; set; } = "Unknown";
public string Version { get; set; } = "0";
public bool HasPlayabilityInfo => PlayabilityStatus != null;
public string LocalizedStatus =>
PlayabilityStatus.HasValue
? LocaleManager.Instance[PlayabilityStatus!.Value]
: string.Empty;
public LocaleKeys? PlayabilityStatus { get; set; }
public int PlayerCount { get; set; }
public int GameCount { get; set; }
public TimeSpan TimePlayed { get; set; }

View File

@@ -6,15 +6,11 @@ namespace Ryujinx.Ava.Utilities
public static class CommandLineState
{
public static string[] Arguments { get; private set; }
public static int CountArguments { get; private set; }
public static bool? OverrideDockedMode { get; private set; }
public static bool? OverrideHardwareAcceleration { get; private set; }
public static string OverrideGraphicsBackend { get; private set; }
public static string OverrideBackendThreading { get; private set; }
public static string OverridePPTC { get; private set; }
public static string OverrideMemoryManagerMode { get; private set; }
public static string OverrideSystemRegion { get; private set; }
public static string OverrideSystemLanguage { get; private set; }
public static string OverrideHideCursor { get; private set; }
public static string BaseDirPathArg { get; private set; }
public static string Profile { get; private set; }
@@ -32,11 +28,6 @@ namespace Ryujinx.Ava.Utilities
{
string arg = args[i];
if (arg.Contains("-") || arg.Contains("--"))
{
CountArguments++;
}
switch (arg)
{
case "-r":
@@ -94,47 +85,6 @@ namespace Ryujinx.Ava.Utilities
OverrideBackendThreading = args[++i];
break;
case "--pptc":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverridePPTC = args[++i];
break;
case "-m":
case "--memory-manager-mode":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideMemoryManagerMode = args[++i];
break;
case "--system-region":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideSystemRegion = args[++i];
break;
case "--system-language":
if (i + 1 >= args.Length)
{
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
continue;
}
OverrideSystemLanguage = args[++i];
break;
case "-i":
case "--application-id":
LaunchApplicationId = args[++i];

View File

@@ -47,11 +47,6 @@ namespace Ryujinx.Ava.Utilities.Compat
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
}
public static void Unload()
{
_entries = null;
}
private static CompatibilityEntry[] _entries;
public static CompatibilityEntry[] Entries
@@ -64,6 +59,11 @@ namespace Ryujinx.Ava.Utilities.Compat
return _entries;
}
}
public static LocaleKeys? GetStatus(string titleId)
=> Entries.FirstOrDefault(x => x.TitleId.HasValue && x.TitleId.Value.EqualsIgnoreCase(titleId))?.Status;
public static LocaleKeys? GetStatus(ulong titleId) => GetStatus(titleId.ToString("X16"));
}
public class CompatibilityEntry

View File

@@ -32,8 +32,6 @@ namespace Ryujinx.Ava.Utilities.Compat
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
CompatibilityCsv.Unload();
}
public CompatibilityList()

View File

@@ -1,10 +1,4 @@
using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Common.Helper;
using System;
using System.Collections.Generic;
using System.Linq;
using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
namespace Ryujinx.Ava.Utilities
{
@@ -13,40 +7,54 @@ namespace Ryujinx.Ava.Utilities
public static PlayReportAnalyzer Analyzer { get; } = new PlayReportAnalyzer()
.AddSpec(
"01007ef00011e000",
spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
spec => spec
.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
// reset to normal status when switching between normal & master mode in title screen
.AddValueFormatter("AoCVer", PlayReportFormattedValue.AlwaysResets)
)
.AddSpec( // Super Mario Odyssey
.AddSpec(
"0100f2c0115b6000",
spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec( // Super Mario Odyssey (China)
"010075000ECBE000",
.AddSpec(
"010075000ecbe000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec( // Super Mario 3D World + Bowser's Fury
"010028600EBDA000",
.AddSpec(
"010028600ebda000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China)
["0100152000022000", "010075100E8EC000"],
.AddSpec( // Global & China IDs
["0100152000022000", "010075100e8ec000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
);
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref PlayReportValue value)
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing Master Mode" : PlayReportFormattedValue.ForceReset;
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value)
private static PlayReportFormattedValue TearsOfTheKingdom_CurrentField(PlayReportValue value) =>
value.PackedValue.AsDouble() switch
{
> 800d => "Exploring the Sky Islands",
< -201d => "Exploring the Depths",
_ => "Roaming Hyrule"
};
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value)
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref PlayReportValue value)
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
=> value.BoxedValue is 0 ? "Playing Super Mario 3D World" : "Playing Bowser's Fury";
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value)
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(PlayReportValue value)
=> value.BoxedValue switch
{
// Single Player
@@ -74,125 +82,4 @@ namespace Ryujinx.Ava.Utilities
_ => PlayReportFormattedValue.ForceReset
};
}
#region Analyzer implementation
public class PlayReportAnalyzer
{
private readonly List<PlayReportGameSpec> _specs = [];
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
{
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
return this;
}
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
{
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
return this;
}
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
{
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..titleIds] }));
return this;
}
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
{
_specs.Add(new PlayReportGameSpec { TitleIds = [..titleIds] }.Apply(transform));
return this;
}
public PlayReportFormattedValue Run(string runningGameId, ApplicationMetadata appMeta, MessagePackObject playReport)
{
if (!playReport.IsDictionary)
return PlayReportFormattedValue.Unhandled;
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
return PlayReportFormattedValue.Unhandled;
foreach (PlayReportValueFormatterSpec formatSpec in spec.Analyses.OrderBy(x => x.Priority))
{
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
continue;
PlayReportValue value = new()
{
Application = appMeta,
BoxedValue = valuePackObject.ToObject()
};
return formatSpec.ValueFormatter(ref value);
}
return PlayReportFormattedValue.Unhandled;
}
}
public class PlayReportGameSpec
{
public required string[] TitleIds { get; init; }
public List<PlayReportValueFormatterSpec> Analyses { get; } = [];
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter valueFormatter)
{
Analyses.Add(new PlayReportValueFormatterSpec
{
Priority = Analyses.Count,
ReportKey = reportKey,
ValueFormatter = valueFormatter
});
return this;
}
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey, PlayReportValueFormatter valueFormatter)
{
Analyses.Add(new PlayReportValueFormatterSpec
{
Priority = priority,
ReportKey = reportKey,
ValueFormatter = valueFormatter
});
return this;
}
}
public struct PlayReportValue
{
public ApplicationMetadata Application { get; init; }
public object BoxedValue { get; init; }
}
public struct PlayReportFormattedValue
{
public bool Handled { get; private init; }
public bool Reset { get; private init; }
public string FormattedString { get; private init; }
public static implicit operator PlayReportFormattedValue(string formattedValue)
=> new() { Handled = true, FormattedString = formattedValue };
public static PlayReportFormattedValue Unhandled => default;
public static PlayReportFormattedValue ForceReset => new() { Handled = true, Reset = true };
public static PlayReportValueFormatter AlwaysResets = AlwaysResetsImpl;
private static PlayReportFormattedValue AlwaysResetsImpl(ref PlayReportValue _) => ForceReset;
}
public struct PlayReportValueFormatterSpec
{
public required int Priority { get; init; }
public required string ReportKey { get; init; }
public PlayReportValueFormatter ValueFormatter { get; init; }
}
public delegate PlayReportFormattedValue PlayReportValueFormatter(ref PlayReportValue value);
#endregion
}

View File

@@ -0,0 +1,274 @@
using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Ryujinx.Ava.Utilities
{
/// <summary>
/// The entrypoint for the Play Report analysis system.
/// </summary>
public class PlayReportAnalyzer
{
private readonly List<PlayReportGameSpec> _specs = [];
/// <summary>
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
/// </summary>
/// <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>
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
public PlayReportAnalyzer AddSpec(string titleId, Func<PlayReportGameSpec, PlayReportGameSpec> transform)
{
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [titleId] }));
return this;
}
/// <summary>
/// Add an analysis spec matching a specific game by title ID, with the provided spec configuration.
/// </summary>
/// <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>
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
public PlayReportAnalyzer AddSpec(string titleId, Action<PlayReportGameSpec> transform)
{
Guard.Ensure(ulong.TryParse(titleId, NumberStyles.HexNumber, null, out _),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
_specs.Add(new PlayReportGameSpec { TitleIds = [titleId] }.Apply(transform));
return this;
}
/// <summary>
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
/// </summary>
/// <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>
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds,
Func<PlayReportGameSpec, PlayReportGameSpec> transform)
{
string[] tids = titleIds.ToArray();
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
_specs.Add(transform(new PlayReportGameSpec { TitleIds = [..tids] }));
return this;
}
/// <summary>
/// Add an analysis spec matching a specific set of games by title IDs, with the provided spec configuration.
/// </summary>
/// <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>
/// <returns>The current <see cref="PlayReportAnalyzer"/>, for chaining convenience.</returns>
public PlayReportAnalyzer AddSpec(IEnumerable<string> titleIds, Action<PlayReportGameSpec> transform)
{
string[] tids = titleIds.ToArray();
Guard.Ensure(tids.All(x => ulong.TryParse(x, NumberStyles.HexNumber, null, out _)),
$"Cannot use a non-hexadecimal string as the Title ID for a {nameof(PlayReportGameSpec)}.");
_specs.Add(new PlayReportGameSpec { TitleIds = [..tids] }.Apply(transform));
return this;
}
/// <summary>
/// Runs the configured <see cref="PlayReportGameSpec.FormatterSpec"/> for the specified game title ID.
/// </summary>
/// <param name="runningGameId">The game currently running.</param>
/// <param name="appMeta">The Application metadata information, including localized game name and play time information.</param>
/// <param name="playReport">The Play Report received from HLE.</param>
/// <returns>A struct representing a possible formatted value.</returns>
public FormattedValue FormatPlayReportValue(
string runningGameId,
ApplicationMetadata appMeta,
MessagePackObject playReport
)
{
if (!playReport.IsDictionary)
return FormattedValue.Unhandled;
if (!_specs.TryGetFirst(s => runningGameId.EqualsAnyIgnoreCase(s.TitleIds), out PlayReportGameSpec spec))
return FormattedValue.Unhandled;
foreach (PlayReportGameSpec.FormatterSpec formatSpec in spec.SimpleValueFormatters.OrderBy(x => x.Priority))
{
if (!playReport.AsDictionary().TryGetValue(formatSpec.ReportKey, out MessagePackObject valuePackObject))
continue;
return formatSpec.ValueFormatter(new PlayReportValue
{
Application = appMeta, PackedValue = valuePackObject
});
}
return FormattedValue.Unhandled;
}
/// <summary>
/// A potential formatted value returned by a <see cref="PlayReportValueFormatter"/>.
/// </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="PlayReportAnalyzer"/> 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; }
public void Match(out bool wasHandled, Action onReset, Action<string> onSuccess)
{
if (!Handled)
{
wasHandled = false;
return;
}
if (Reset)
onReset();
else
onSuccess(FormattedString);
wasHandled = true;
}
/// <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="PlayReportAnalyzer"/> 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="PlayReportValueFormatter"/>.
/// </summary>
public static readonly PlayReportValueFormatter AlwaysResets = _ => ForceReset;
}
}
/// <summary>
/// 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>
/// </summary>
public class PlayReportGameSpec
{
public required string[] TitleIds { get; init; }
public List<FormatterSpec> SimpleValueFormatters { get; } = [];
/// <summary>
/// Add a value formatter to the current <see cref="PlayReportGameSpec"/>
/// 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="PlayReportGameSpec"/>, for chaining convenience.</returns>
public PlayReportGameSpec AddValueFormatter(string reportKey, PlayReportValueFormatter 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="PlayReportGameSpec"/>
/// 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="PlayReportGameSpec"/>, for chaining convenience.</returns>
public PlayReportGameSpec AddValueFormatter(int priority, string reportKey,
PlayReportValueFormatter valueFormatter)
{
SimpleValueFormatters.Add(new FormatterSpec
{
Priority = priority, ReportKey = reportKey, 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 PlayReportValueFormatter ValueFormatter { get; init; }
}
}
/// <summary>
/// The input data to a <see cref="PlayReportValueFormatter"/>,
/// containing the currently running application's <see cref="ApplicationMetadata"/>,
/// and the matched <see cref="MessagePackObject"/> from the Play Report.
/// </summary>
public class PlayReportValue
{
/// <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 <see cref="PackedValue"/> and the AsX (where X is a numerical type name i.e. Int32) methods for that.
/// </summary>
public object BoxedValue => PackedValue.ToObject();
}
/// <summary>
/// The delegate type that powers the entire analysis system (as it currently is).<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="PlayReportAnalyzer"/> for.
/// </summary>
public delegate PlayReportAnalyzer.FormattedValue PlayReportValueFormatter(PlayReportValue value);
}

View File

@@ -12,7 +12,7 @@ namespace Ryujinx.Ava.Utilities
public static class ShortcutHelper
{
[SupportedOSPlatform("windows")]
private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath, string args = "")
private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe");
iconPath += ".ico";
@@ -22,13 +22,13 @@ namespace Ryujinx.Ava.Utilities
image.Resize(new SKImageInfo(128, 128), SKFilterQuality.High);
SaveBitmapAsIcon(image, iconPath);
Shortcut shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId, args), iconPath, 0);
Shortcut shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId), iconPath, 0);
shortcut.StringData.NameString = cleanedAppName;
shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
}
[SupportedOSPlatform("linux")]
private static void CreateShortcutLinux(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName, string args = "")
private static void CreateShortcutLinux(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh");
string desktopFile = EmbeddedResources.ReadAllText("Ryujinx/Assets/ShortcutFiles/shortcut-template.desktop");
@@ -40,11 +40,11 @@ namespace Ryujinx.Ava.Utilities
data.SaveTo(file);
using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId, args)}");
outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId)}");
}
[SupportedOSPlatform("macos")]
private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName, string args = "")
private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
string plistFile = EmbeddedResources.ReadAllText("Ryujinx/Assets/ShortcutFiles/shortcut-template.plist");
@@ -63,7 +63,7 @@ namespace Ryujinx.Ava.Utilities
string scriptPath = Path.Combine(scriptFolderPath, ScriptName);
using StreamWriter scriptFile = new(scriptPath);
scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId, args));
scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId));
// Set execute permission
FileInfo fileInfo = new(scriptPath);
@@ -87,7 +87,7 @@ namespace Ryujinx.Ava.Utilities
outputFile.Write(plistFile, ScriptName, cleanedAppName, IconName);
}
public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData, string args = "")
public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData)
{
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
string cleanedAppName = string.Join("_", applicationName.Split(Path.GetInvalidFileNameChars()));
@@ -96,7 +96,7 @@ namespace Ryujinx.Ava.Utilities
{
string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath, args);
CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath);
return;
}
@@ -106,14 +106,14 @@ namespace Ryujinx.Ava.Utilities
string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx");
Directory.CreateDirectory(iconPath);
CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName, args);
CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
return;
}
if (OperatingSystem.IsMacOS())
{
CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName, args);
CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName);
return;
}
@@ -121,7 +121,7 @@ namespace Ryujinx.Ava.Utilities
throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
}
private static string GetArgsString(string appFilePath, string applicationId, string config = "")
private static string GetArgsString(string appFilePath, string applicationId)
{
// args are first defined as a list, for easier adjustments in the future
List<string> argsList = [];
@@ -132,11 +132,6 @@ namespace Ryujinx.Ava.Utilities
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
}
if (!string.IsNullOrEmpty(config))
{
argsList.Add(config);
}
if (appFilePath.ToLower().EndsWith(".xci"))
{
argsList.Add("--application-id");