Compare commits

..

17 Commits

Author SHA1 Message Date
Vladimir Sokolov
2d30adb3ac Merge branch 'master' into master 2025-02-03 18:06:19 +10:00
Vova
d679725334 "IsMacOS" to "RunningPlatform.IsMacOS" 2025-02-03 17:05:59 +10:00
Vladimir Sokolov
6e5861d768 Merge branch 'master' into master 2025-02-03 15:52:42 +10:00
Vladimir Sokolov
4efd20c54b Merge branch 'master' into master 2025-02-03 15:33:55 +10:00
Vladimir Sokolov
2e13cdce0c Merge branch 'master' into master 2025-02-02 19:51:12 +10:00
Vladimir Sokolov
b742bcb75a Merge branch 'master' into master 2025-02-02 11:31:31 +10:00
Vladimir Sokolov
068f6be609 Merge branch 'Ryubing:master' into master 2025-02-02 09:41:31 +10:00
Vova
03b4a2412b Added ReleaseInformation 2025-02-01 22:32:07 +10:00
Vova
9397ef8a8a Merge branch 'master' of https://github.com/Goodfeat/Ryujinx_alt 2025-02-01 21:39:54 +10:00
Vova
720a81ef6f Add ConfigFileNameOverride in ReleaseInformation 2025-02-01 21:38:12 +10:00
Vladimir Sokolov
25c47368db Merge branch 'master' into master 2025-02-01 20:52:57 +10:00
Vladimir Sokolov
32df1da773 Merge branch 'master' into master 2025-02-01 17:45:47 +10:00
Vova
3a8737f0e6 fixed bugs and renamed method to GetArguments 2025-02-01 17:07:04 +10:00
Vova
d8cf67d358 Merge branch 'master' of https://github.com/Goodfeat/Ryujinx_alt 2025-02-01 16:25:04 +10:00
Vladimir Sokolov
fd258a1f6a Merge branch 'Ryubing:master' into master 2025-02-01 16:22:54 +10:00
Vova
b8fe778299 Added a window with settings for shortcuts.
Added new arguments for shortcuts.
When starting a game with arguments, a separate independent setting will be used
2025-02-01 16:21:32 +10:00
Vova
d76da6e4df Added --backend-threading parameter for launching games via shortcut 2025-01-29 21:45:48 +10:00
32 changed files with 948 additions and 837 deletions

View File

@@ -13,12 +13,14 @@ 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("%%") &&
@@ -26,7 +28,8 @@ namespace Ryujinx.Common
!ReleaseChannelOwner.StartsWith("%%") &&
!ReleaseChannelSourceRepo.StartsWith("%%") &&
!ReleaseChannelRepo.StartsWith("%%") &&
!ConfigFileName.StartsWith("%%");
!ConfigFileName.StartsWith("%%") &&
!ConfigFileNameOverride.StartsWith("%%");
public static bool IsCanaryBuild => IsValid && ReleaseChannelName.Equals(CanaryChannel);

View File

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

View File

@@ -1,4 +1,3 @@
using Gommon;
using MsgPack;
using MsgPack.Serialization;
using Ryujinx.Common.Logging;
@@ -12,7 +11,6 @@ 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

@@ -2522,106 +2522,6 @@
"zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式"
}
},
{
"ID": "GameListContextMenuShowCompatEntry",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show Compatibility Entry",
"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": "GameListContextMenuShowCompatEntryToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show the selected game in the Compatibility List you can normally access via the Help menu.",
"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": "GameListContextMenuShowGameData",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show Game Stats",
"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": "GameListContextMenuShowGameDataToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show the other various information about the currently selected game that is missing from the Grid view layout.",
"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": "GameListContextMenuOpenModsDirectory",
"Translations": {
@@ -23169,7 +23069,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "可游玩",
"zh_TW": "可暢順遊玩"
"zh_TW": "可暢順遊玩 (Playable)"
}
},
{
@@ -23194,7 +23094,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "进入游戏",
"zh_TW": "大致可遊玩"
"zh_TW": "大致可遊玩 (Ingame)"
}
},
{
@@ -23219,7 +23119,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "菜单",
"zh_TW": "只開啟至遊戲開始功能表"
"zh_TW": "只開啟至遊戲開始功能表 (Menus)"
}
},
{
@@ -23244,7 +23144,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "启动",
"zh_TW": "只能啟動"
"zh_TW": "只能啟動 (Boots)"
}
},
{
@@ -23269,7 +23169,7 @@
"tr_TR": "",
"uk_UA": "",
"zh_CN": "什么都没有",
"zh_TW": "無法啟動"
"zh_TW": "無法啟動 (Nothing)"
}
},
{

View File

@@ -56,7 +56,7 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
HorizonStatic.PlayReport += HandlePlayReport;
HorizonStatic.PlayReportPrinted += HandlePlayReport;
}
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
@@ -117,6 +117,11 @@ namespace Ryujinx.Ava
_currentApp = appMeta;
}
private static void UpdatePlayingState()
{
_discordClient?.SetPresence(_discordPresencePlaying);
}
private static void SwitchToMainState()
{
_discordClient?.SetPresence(_discordPresenceMain);
@@ -130,20 +135,21 @@ namespace Ryujinx.Ava
if (!TitleIDs.CurrentApplication.Value.HasValue) return;
if (_discordPresencePlaying is null) return;
PlayReportAnalyzer.FormattedValue formattedValue =
PlayReport.Analyzer.Format(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
PlayReportFormattedValue value = PlayReport.Analyzer.Run(TitleIDs.CurrentApplication.Value, _currentApp, playReport);
if (!formattedValue.Handled) return;
if (!value.Handled) return;
_discordPresencePlaying.Details = formattedValue.Reset
? $"Playing {_currentApp.Title}"
: formattedValue.FormattedString;
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
return; //don't trigger an update if the set presence Details are identical to current
_discordClient.SetPresence(_discordPresencePlaying);
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
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();
}
private static string TruncateToByteLength(string input)

View File

@@ -160,6 +160,28 @@ 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))
{
@@ -232,8 +254,35 @@ 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 hardware-acceleration was overridden.
// 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! )
if (CommandLineState.OverrideHardwareAcceleration != null)
UseHardwareAcceleration = CommandLineState.OverrideHardwareAcceleration.Value;
}

View File

@@ -32,9 +32,6 @@ namespace Ryujinx.Ava
public static MainWindow MainWindow => Current!
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
.MainWindow.Cast<MainWindow>();
public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current!
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>();
public static bool IsClipboardAvailable(out IClipboard clipboard)
{

View File

@@ -19,18 +19,6 @@
Header="{ext:Locale GameListContextMenuCreateShortcut}"
Icon="{ext:Icon fa-solid fa-bookmark}"
ToolTip.Tip="{OnPlatform Default={ext:Locale GameListContextMenuCreateShortcutToolTip}, macOS={ext:Locale GameListContextMenuCreateShortcutToolTipMacOS}}" />
<MenuItem
IsVisible="{Binding HasCompatibilityEntry}"
Click="OpenApplicationCompatibility_Click"
Header="{ext:Locale GameListContextMenuShowCompatEntry}"
Icon="{ext:Icon mdi-gamepad}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
<MenuItem
IsVisible="{Binding IsGrid}"
Click="OpenApplicationData_Click"
Header="{ext:Locale GameListContextMenuShowGameData}"
Icon="{ext:Icon mdi-chart-line}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowGameDataToolTip}"/>
<Separator />
<MenuItem
Click="OpenUserSaveDirectory_Click"
@@ -86,6 +74,7 @@
Header="{ext:Locale GameListContextMenuTrimXCI}"
IsEnabled="{Binding TrimXCIEnabled}"
ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" />
<Separator />
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon mdi-cached}">
<MenuItem
Click="PurgePtcCache_Click"
@@ -123,7 +112,6 @@
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
IsVisible="{Binding HasDlc}"
Click="ExtractAocRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />

View File

@@ -12,7 +12,6 @@ using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.HLE.HOS;
@@ -334,7 +333,7 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return;
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary);
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
if (selectedDlc is not null)
{
@@ -376,27 +375,11 @@ namespace Ryujinx.Ava.UI.Controls
png.SaveTo(fileStream);
}
public void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args)
public async void CreateApplicationShortcut_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
ShortcutHelper.CreateAppShortcut(
viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.Name,
viewModel.SelectedApplication.IdString,
viewModel.SelectedApplication.Icon
);
}
public async void OpenApplicationCompatibility_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await CompatibilityList.Show(viewModel.SelectedApplication.IdString);
}
public async void OpenApplicationData_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await ApplicationDataView.Show(viewModel.SelectedApplication);
await new ArgumentsConfigWindows(viewModel).ShowDialog((Window)viewModel.TopLevel);
}
public async void RunApplication_Click(object sender, RoutedEventArgs args)
@@ -407,8 +390,12 @@ namespace Ryujinx.Ava.UI.Controls
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
}
}
}
}

View File

@@ -1,116 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:appLibrary="using:Ryujinx.Ava.Utilities.AppLibrary"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
x:DataType="appLibrary:ApplicationData">
<StackPanel Orientation="Vertical">
<Grid
RowDefinitions="Auto, Auto, Auto, Auto"
ColumnDefinitions="*">
<TextBlock Grid.Row="0"
HorizontalAlignment="Center"
Padding="0, -2, 0, 5"
Classes="h1"
Text="{Binding Name}" />
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
Text="{Binding Version}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock Grid.Row="2"
HorizontalAlignment="Center"
Text="{Binding Developer}"
TextAlignment="Start"
TextWrapping="Wrap" />
<StackPanel Grid.ColumnSpan="2" Grid.Row="3"
HorizontalAlignment="Center"
Orientation="Horizontal" Margin="0 10, 0, 0"
Spacing="5">
<Button
Click="PlayabilityStatus_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
Tag="{Binding IdString}"
Text="{Binding LocalizedStatus}"
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button.Styles>
<Style Selector="Button">
<Setter Property="MinWidth"
Value="0" />
<!-- avoids very wide buttons from the overall project avalonia style -->
</Style>
</Button.Styles>
</Button>
<Button
Click="IdString_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
HorizontalAlignment="Stretch"
Text="{Binding IdString}"
TextAlignment="Start"
TextWrapping="Wrap" />
</Button>
</StackPanel>
</Grid>
<Separator Margin="0, 10, 0, 10" />
<Grid ColumnDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0"
Margin="10,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FileExtension}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding Converter={helpers:MultiplayerInfoConverter}}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel
Grid.Column="2"
Margin="0,0,10,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding LastPlayedString}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding TimePlayedString}"
IsVisible="{Binding HasPlayedPreviously}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FileSizeString}"
TextAlignment="End"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View File

@@ -1,84 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Controls
{
public partial class ApplicationDataView : UserControl
{
public static async Task Show(ApplicationData appData)
{
ContentDialog contentDialog = new()
{
PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
Content = new ApplicationDataView { DataContext = appData }
};
Style closeButton = new(x => x.Name("CloseButton"));
closeButton.Setters.Add(new Setter(WidthProperty, 160d));
Style closeButtonParent = new(x => x.Name("CommandSpace"));
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
Avalonia.Layout.HorizontalAlignment.Center));
contentDialog.Styles.Add(closeButton);
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
}
public ApplicationDataView()
{
InitializeComponent();
}
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
{
if (sender is not Button { Content: TextBlock playabilityLabel })
return;
if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window))
window.Close(ContentDialogResult.None);
await CompatibilityList.Show((string)playabilityLabel.Tag);
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel mwvm)
return;
if (sender is not Button { Content: TextBlock idText })
return;
if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard))
return;
ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text);
if (appData is null)
return;
await clipboard.SetTextAsync(appData.IdString);
NotificationHelper.ShowInformation(
"Copied Title ID",
$"{appData.Name} ({appData.IdString})");
}
}
}

View File

@@ -86,29 +86,6 @@
Text="{Binding Version}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button
Click="PlayabilityStatus_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Margin="-1, 0, 0, 0"
Padding="0" >
<TextBlock
Margin="1.5"
Tag="{Binding IdString}"
Text="{Binding LocalizedStatus}"
Foreground="{Binding PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button.Styles>
<Style Selector="Button">
<Setter Property="MinWidth"
Value="0" />
<!-- avoids very wide buttons from the overall project avalonia style -->
</Style>
</Button.Styles>
</Button>
</StackPanel>
</Border>
<StackPanel

View File

@@ -5,9 +5,7 @@ using Avalonia.Interactivity;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using System;
using System.Globalization;
using System.Linq;
namespace Ryujinx.Ava.UI.Controls
@@ -30,17 +28,6 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is ListBox { SelectedItem: ApplicationData selected })
RaiseEvent(new ApplicationOpenedEventArgs(selected, ApplicationOpenedEvent));
}
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel mwvm)
return;
if (sender is not Button { Content: TextBlock playabilityLabel })
return;
await CompatibilityList.Show((string)playabilityLabel.Tag);
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)
{

View File

@@ -14,7 +14,9 @@ namespace Ryujinx.Ava.UI.ViewModels
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
{
_dlcs = appLibrary.FindDlcsFor(titleId)
_dlcs = appLibrary.DownloadableContents.Items
.Where(x => x.Dlc.TitleIdBase == titleId)
.Select(x => x.Dlc)
.OrderBy(it => it.IsBundled ? 0 : 1)
.ThenBy(it => it.TitleId)
.ToArray();

View File

@@ -69,7 +69,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadDownloadableContents()
{
(DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id);
IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
bool hasBundledContent = false;
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)

View File

@@ -349,10 +349,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo;
public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id);
public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;
public bool OpenDeviceSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.DeviceSaveDataSize > 0;

View File

@@ -1,8 +1,10 @@
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;
@@ -26,6 +28,7 @@ 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;
@@ -63,12 +66,56 @@ 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
{
@@ -344,6 +391,30 @@ 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 = [];
@@ -716,6 +787,11 @@ 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

@@ -41,7 +41,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadUpdates()
{
(TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id);
IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items
.Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
bool hasBundledContent = false;
SelectedUpdate = new TitleUpdateViewModelNoUpdate();

View File

@@ -50,7 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Main
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show);
AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show);
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityList.Show());
CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show);
UpdateMenuItem.Command = Commands.Create(async () =>
{

View File

@@ -0,0 +1,237 @@
<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"
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
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="{x:Static helper:RunningPlatform.IsArmMac}"
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="{x:Static helper:RunningPlatform.IsArmMac}">
<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

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

View File

@@ -0,0 +1,105 @@
<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"
xmlns:helper="clr-namespace:Ryujinx.Common.Helper;assembly=Ryujinx.Common"
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="{x:Static helper:RunningPlatform.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

@@ -0,0 +1,162 @@
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,8 +7,6 @@ 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;
@@ -23,30 +21,9 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public bool Favorite { get; set; }
public byte[] Icon { get; set; }
public string Name { get; set; } = "Unknown";
private ulong _id;
public ulong Id
{
get => _id;
set
{
_id = value;
PlayabilityStatus = CompatibilityCsv.GetStatus(Id);
}
}
public ulong Id { get; set; }
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

@@ -128,50 +128,13 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
if (appData.HasValue)
return appData.Value.Name;
if (!DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
return id.ToString("X16");
string name = Path.GetFileNameWithoutExtension(dlcData.FileName)!;
int idx = name.IndexOf('[');
if (idx != -1)
name = name[..idx];
if (DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
return Path.GetFileNameWithoutExtension(dlcData.FileName);
return name;
return id.ToString("X16");
}
public bool FindApplication(ulong id, out ApplicationData foundData)
{
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
foundData = appData.HasValue ? appData.Value : null;
return appData.HasValue;
}
public bool FindUpdate(ulong id, out TitleUpdateModel foundData)
{
Gommon.Optional<TitleUpdateModel> appData =
TitleUpdates.Keys.FindFirst(x => x.TitleId == id);
foundData = appData.HasValue ? appData.Value : null;
return appData.HasValue;
}
public TitleUpdateModel[] FindUpdatesFor(ulong id)
=> TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (TitleUpdateModel TitleUpdate, bool IsSelected)[] FindUpdateConfigurationFor(ulong id)
=> TitleUpdates.Items.Where(x => x.TitleUpdate.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public DownloadableContentModel[] FindDlcsFor(ulong id)
=> DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (DownloadableContentModel Dlc, bool IsEnabled)[] FindDlcConfigurationFor(ulong id)
=> DownloadableContents.Items.Where(x => x.Dlc.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public bool HasDlcs(ulong id)
=> DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL));
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>
/// <exception cref="NotSupportedException">The NCA version is not supported.</exception>

View File

@@ -6,11 +6,15 @@ 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; }
@@ -28,6 +32,11 @@ namespace Ryujinx.Ava.Utilities
{
string arg = args[i];
if (arg.Contains("-") || arg.Contains("--"))
{
CountArguments++;
}
switch (arg)
{
case "-r":
@@ -85,6 +94,47 @@ 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,6 +47,11 @@ 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
@@ -59,11 +64,6 @@ 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
@@ -113,17 +113,20 @@ namespace Ryujinx.Ava.Utilities.Compat
.Select(FormatLabelName)
.JoinToString(", ");
public override string ToString() =>
new StringBuilder("CompatibilityEntry: {")
.Append($"{nameof(GameName)}=\"{GameName}\", ")
.Append($"{nameof(TitleId)}={TitleId}, ")
.Append($"{nameof(Labels)}={
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
}, ")
.Append($"{nameof(Status)}=\"{Status}\", ")
.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"")
.Append('}')
.ToString();
public override string ToString()
{
StringBuilder sb = new("CompatibilityEntry: {");
sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
sb.Append($"{nameof(TitleId)}={TitleId}, ");
sb.Append($"{nameof(Labels)}={
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
}, ");
sb.Append($"{nameof(Status)}=\"{Status}\", ");
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
sb.Append('}');
return sb.ToString();
}
public static string FormatLabelName(string labelName) => labelName.ToLower() switch
{

View File

@@ -34,7 +34,7 @@
Text="{ext:Locale CompatibilityListWarning}" />
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto">
<TextBox Name="SearchBox" Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
<TextBox Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
<CheckBox Grid.Column="1" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
<TextBlock Grid.Column="2" Margin="-10, 0, 0, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</Grid>

View File

@@ -9,7 +9,7 @@ namespace Ryujinx.Ava.Utilities.Compat
{
public partial class CompatibilityList : UserControl
{
public static async Task Show(string titleId = null)
public static async Task Show()
{
ContentDialog contentDialog = new()
{
@@ -18,10 +18,7 @@ namespace Ryujinx.Ava.Utilities.Compat
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
Content = new CompatibilityList
{
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary),
SearchBox = {
Text = titleId ?? ""
}
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary)
}
};
@@ -35,6 +32,8 @@ namespace Ryujinx.Ava.Utilities.Compat
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
CompatibilityCsv.Unload();
}
public CompatibilityList()

View File

@@ -1,4 +1,10 @@
using PlayReportFormattedValue = Ryujinx.Ava.Utilities.PlayReportAnalyzer.FormattedValue;
using Gommon;
using MsgPack;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Common.Helper;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities
{
@@ -7,55 +13,41 @@ namespace Ryujinx.Ava.Utilities
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)
spec => spec.AddValueFormatter("IsHardMode", BreathOfTheWild_MasterMode)
)
.AddSpec(
"0100f2c0115b6000",
spec => spec.AddValueFormatter("PlayerPosY", TearsOfTheKingdom_CurrentField))
.AddSpec(
.AddSpec( // Super Mario Odyssey
"0100000000010000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdyssey_AssistMode)
)
.AddSpec(
"010075000ecbe000",
.AddSpec( // Super Mario Odyssey (China)
"010075000ECBE000",
spec =>
spec.AddValueFormatter("is_kids_mode", SuperMarioOdysseyChina_AssistMode)
)
.AddSpec(
"010028600ebda000",
.AddSpec( // Super Mario 3D World + Bowser's Fury
"010028600EBDA000",
spec => spec.AddValueFormatter("mode", SuperMario3DWorldOrBowsersFury)
)
.AddSpec( // Global & China IDs
["0100152000022000", "010075100e8ec000"],
.AddSpec( // Mario Kart 8 Deluxe, Mario Kart 8 Deluxe (China)
["0100152000022000", "010075100E8EC000"],
spec => spec.AddValueFormatter("To", MarioKart8Deluxe_Mode)
);
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(PlayReportValue value)
private static PlayReportFormattedValue BreathOfTheWild_MasterMode(ref 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)
private static PlayReportFormattedValue SuperMarioOdyssey_AssistMode(ref PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in Assist Mode" : "Playing in Regular Mode";
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(PlayReportValue value)
private static PlayReportFormattedValue SuperMarioOdysseyChina_AssistMode(ref PlayReportValue value)
=> value.BoxedValue is 1 ? "Playing in 帮助模式" : "Playing in 普通模式";
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(PlayReportValue value)
private static PlayReportFormattedValue SuperMario3DWorldOrBowsersFury(ref 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
private static PlayReportFormattedValue MarioKart8Deluxe_Mode(ref PlayReportValue value)
=> value.BoxedValue switch
{
// Single Player
"Single" => "Single Player",
@@ -82,4 +74,125 @@ 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

@@ -1,282 +0,0 @@
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 Format(
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; }
/// <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 delegate factory you can use to always return the specified
/// <paramref name="formattedValue"/> in a <see cref="PlayReportValueFormatter"/>.
/// </summary>
/// <param name="formattedValue">The string to always return for this delegate instance.</param>
public static PlayReportValueFormatter AlwaysReturns(string formattedValue) => _ => formattedValue;
}
}
/// <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();
#region AsX accessors
public bool BooleanValue => PackedValue.AsBoolean();
public byte ByteValye => PackedValue.AsByte();
public sbyte SByteValye => PackedValue.AsSByte();
public short ShortValye => PackedValue.AsInt16();
public ushort UShortValye => PackedValue.AsUInt16();
public int IntValye => PackedValue.AsInt32();
public uint UIntValye => PackedValue.AsUInt32();
public long LongValye => PackedValue.AsInt64();
public ulong ULongValye => PackedValue.AsUInt64();
public float FloatValue => PackedValue.AsSingle();
public double DoubleValue => PackedValue.AsDouble();
public string StringValue => PackedValue.AsString();
public Span<byte> BinaryValue => PackedValue.AsBinary();
#endregion
}
/// <summary>
/// The delegate type that powers 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)
private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath, string args = "")
{
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), iconPath, 0);
Shortcut shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId, args), 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)
private static void CreateShortcutLinux(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName, string args = "")
{
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)}");
outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId, args)}");
}
[SupportedOSPlatform("macos")]
private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName)
private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName, string args = "")
{
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));
scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId, args));
// 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)
public static void CreateAppShortcut(string applicationFilePath, string applicationName, string applicationId, byte[] iconData, string args = "")
{
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);
CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath, args);
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);
CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName, args);
return;
}
if (OperatingSystem.IsMacOS())
{
CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName);
CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName, args);
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)
private static string GetArgsString(string appFilePath, string applicationId, string config = "")
{
// args are first defined as a list, for easier adjustments in the future
List<string> argsList = [];
@@ -132,6 +132,11 @@ namespace Ryujinx.Ava.Utilities
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
}
if (!string.IsNullOrEmpty(config))
{
argsList.Add(config);
}
if (appFilePath.ToLower().EndsWith(".xci"))
{
argsList.Add("--application-id");