mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2025-07-30 09:39:56 -06:00
Compare commits
33 Commits
Canary-1.2
...
Canary-1.2
Author | SHA1 | Date | |
---|---|---|---|
cc95e80ee9 | |||
d75ce52bd4 | |||
4a4ea557de | |||
33f42adb11 | |||
918ec1bde3 | |||
cca429d46a | |||
845c86f545 | |||
27993b789f | |||
bdd890cf6f | |||
c5574b41a1 | |||
292e27f0da | |||
606e149bd3 | |||
a8c3407d11 | |||
daa8168985 | |||
f580521e99 | |||
2226521f6c | |||
384416953d | |||
1343fabe41 | |||
1e52af5e29 | |||
672f5df0f9 | |||
804d9c1efe | |||
9270b35648 | |||
5a6d01db3c | |||
ef9c1416ec | |||
5efa7d5dfa | |||
a82569d615 | |||
ed5832ca73 | |||
574aa9ff9c | |||
8a29428de2 | |||
f4272b05fa | |||
d8265f7772 | |||
259526430c | |||
b5fafb6394 |
@ -42,7 +42,7 @@
|
||||
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
|
||||
<PackageVersion Include="Gommon" Version="2.7.0.1" />
|
||||
<PackageVersion Include="Gommon" Version="2.7.0.2" />
|
||||
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
||||
<PackageVersion Include="Sep" Version="0.6.0" />
|
||||
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
||||
|
3422
docs/compatibility.csv
Normal file
3422
docs/compatibility.csv
Normal file
File diff suppressed because it is too large
Load Diff
@ -284,7 +284,7 @@ namespace Ryujinx.HLE.HOS
|
||||
ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
|
||||
|
||||
uint[] defaultCapabilities = {
|
||||
0x030363F7,
|
||||
(((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u,
|
||||
0x1FFFFFCF,
|
||||
0x207FFFEF,
|
||||
0x47E0060F,
|
||||
|
@ -63,6 +63,7 @@ namespace Ryujinx.HLE.HOS.Kernel
|
||||
TickSource = tickSource;
|
||||
Device = device;
|
||||
Memory = memory;
|
||||
KScheduler.CpuCoresCount = device.CpuCoresCount;
|
||||
|
||||
Running = true;
|
||||
|
||||
|
@ -37,7 +37,7 @@ namespace Ryujinx.HLE.HOS.Kernel
|
||||
return result;
|
||||
}
|
||||
|
||||
process.DefaultCpuCore = 3;
|
||||
process.DefaultCpuCore = KScheduler.CpuCoresCount - 1;
|
||||
|
||||
context.Processes.TryAdd(process.Pid, process);
|
||||
|
||||
|
@ -277,7 +277,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
return result;
|
||||
}
|
||||
|
||||
result = Capabilities.InitializeForUser(capabilities, MemoryManager);
|
||||
result = Capabilities.InitializeForUser(capabilities, MemoryManager, IsApplication);
|
||||
|
||||
if (result != Result.Success)
|
||||
{
|
||||
|
@ -35,15 +35,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
DebuggingFlags &= ~3u;
|
||||
KernelReleaseVersion = KProcess.KernelVersionPacked;
|
||||
|
||||
return Parse(capabilities, memoryManager);
|
||||
return Parse(capabilities, memoryManager, false);
|
||||
}
|
||||
|
||||
public Result InitializeForUser(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager)
|
||||
public Result InitializeForUser(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager, bool isApplication)
|
||||
{
|
||||
return Parse(capabilities, memoryManager);
|
||||
return Parse(capabilities, memoryManager, isApplication);
|
||||
}
|
||||
|
||||
private Result Parse(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager)
|
||||
private Result Parse(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager, bool isApplication)
|
||||
{
|
||||
int mask0 = 0;
|
||||
int mask1 = 0;
|
||||
@ -54,7 +54,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
|
||||
if (cap.GetCapabilityType() != CapabilityType.MapRange)
|
||||
{
|
||||
Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager);
|
||||
Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager, isApplication);
|
||||
|
||||
if (result != Result.Success)
|
||||
{
|
||||
@ -120,7 +120,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager)
|
||||
private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager, bool isApplication)
|
||||
{
|
||||
CapabilityType code = cap.GetCapabilityType();
|
||||
|
||||
@ -176,6 +176,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
AllowedCpuCoresMask = GetMaskFromMinMax(lowestCpuCore, highestCpuCore);
|
||||
AllowedThreadPriosMask = GetMaskFromMinMax(lowestThreadPrio, highestThreadPrio);
|
||||
|
||||
if (isApplication && lowestCpuCore == 0 && highestCpuCore != 2)
|
||||
Ryujinx.Common.Logging.Logger.Error?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}! Report this to @LotP on the Ryujinx/Ryubing discord server (discord.gg/ryujinx)!");
|
||||
else if (isApplication)
|
||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -2683,7 +2683,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall
|
||||
return KernelResult.InvalidCombination;
|
||||
}
|
||||
|
||||
if ((uint)preferredCore > 3)
|
||||
if ((uint)preferredCore > KScheduler.CpuCoresCount - 1)
|
||||
{
|
||||
if ((preferredCore | 2) != -1)
|
||||
{
|
||||
|
@ -9,13 +9,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
||||
partial class KScheduler : IDisposable
|
||||
{
|
||||
public const int PrioritiesCount = 64;
|
||||
public const int CpuCoresCount = 4;
|
||||
public static int CpuCoresCount;
|
||||
|
||||
private const int RoundRobinTimeQuantumMs = 10;
|
||||
|
||||
private static readonly int[] _preemptionPriorities = { 59, 59, 59, 63 };
|
||||
|
||||
private static readonly int[] _srcCoresHighestPrioThreads = new int[CpuCoresCount];
|
||||
private static int[] _srcCoresHighestPrioThreads;
|
||||
|
||||
private readonly KernelContext _context;
|
||||
private readonly int _coreId;
|
||||
@ -47,6 +45,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
||||
_coreId = coreId;
|
||||
|
||||
_currentThread = null;
|
||||
|
||||
if (_srcCoresHighestPrioThreads == null)
|
||||
{
|
||||
_srcCoresHighestPrioThreads = new int[CpuCoresCount];
|
||||
}
|
||||
}
|
||||
|
||||
private static int PreemptionPriorities(int index)
|
||||
{
|
||||
return index == CpuCoresCount - 1 ? 63 : 59;
|
||||
}
|
||||
|
||||
public static ulong SelectThreads(KernelContext context)
|
||||
@ -437,7 +445,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
||||
|
||||
for (int core = 0; core < CpuCoresCount; core++)
|
||||
{
|
||||
RotateScheduledQueue(context, core, _preemptionPriorities[core]);
|
||||
RotateScheduledQueue(context, core, PreemptionPriorities(core));
|
||||
}
|
||||
|
||||
context.CriticalSection.Leave();
|
||||
|
@ -24,14 +24,14 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
// not large enough.
|
||||
private const int PointerBufferSize = 0x8000;
|
||||
|
||||
private readonly static uint[] _defaultCapabilities = {
|
||||
0x030363F7,
|
||||
private static uint[] _defaultCapabilities => [
|
||||
(((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u,
|
||||
0x1FFFFFCF,
|
||||
0x207FFFEF,
|
||||
0x47E0060F,
|
||||
0x0048BFFF,
|
||||
0x01007FFF,
|
||||
};
|
||||
];
|
||||
|
||||
// The amount of time Dispose() will wait to Join() the thread executing the ServerLoop()
|
||||
private static readonly TimeSpan _threadJoinTimeout = TimeSpan.FromSeconds(3);
|
||||
|
@ -32,6 +32,8 @@ namespace Ryujinx.HLE
|
||||
public TamperMachine TamperMachine { get; }
|
||||
public IHostUIHandler UIHandler { get; }
|
||||
|
||||
public int CpuCoresCount = 4; //Switch 1 has 4 cores
|
||||
|
||||
public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch;
|
||||
public bool CustomVSyncIntervalEnabled { get; set; } = false;
|
||||
public int CustomVSyncInterval { get; set; }
|
||||
|
@ -402,7 +402,7 @@
|
||||
<x:Double x:Key="ControlContentThemeFontSize">13</x:Double>
|
||||
<x:Double x:Key="MenuItemHeight">26</x:Double>
|
||||
<x:Double x:Key="TabItemMinHeight">28</x:Double>
|
||||
<x:Double x:Key="ContentDialogMaxWidth">700</x:Double>
|
||||
<x:Double x:Key="ContentDialogMaxWidth">900</x:Double>
|
||||
<x:Double x:Key="ContentDialogMaxHeight">756</x:Double>
|
||||
</Styles.Resources>
|
||||
</Styles>
|
||||
|
@ -22597,6 +22597,56 @@
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListLastUpdated",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Last updated: {0}",
|
||||
"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": "CompatibilityListWarning",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "This compatibility list might contain out of date entries.\nDo not be opposed to testing games in the \"Ingame\" status.",
|
||||
"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": "CompatibilityListSearchBoxWatermark",
|
||||
"Translations": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
using DiscordRPC;
|
||||
using DiscordRPC;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Audio.Backends.SDL2;
|
||||
using Ryujinx.Ava;
|
||||
|
@ -145,6 +145,9 @@
|
||||
<EmbeddedResource Include="..\..\distribution\macos\shortcut-template.plist">
|
||||
<Link>Assets\ShortcutFiles\shortcut-template.plist</Link>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="..\..\docs\compatibility.csv" LogicalName="RyujinxGameCompatibilityList">
|
||||
<Link>Assets\RyujinxGameCompatibility.csv</Link>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Assets\locales.json" />
|
||||
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
|
||||
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
|
||||
@ -168,12 +171,6 @@
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Assets\locales.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Utilities\Compat\CompatibilityContentDialog.axaml.cs">
|
||||
<DependentUpon>CompatibilityContentDialog.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Assets\Fonts\Mono\" />
|
||||
</ItemGroup>
|
||||
|
@ -133,12 +133,13 @@
|
||||
Spacing="5">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding TimePlayedString}"
|
||||
Text="{Binding LastPlayedString}"
|
||||
TextAlignment="End"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding LastPlayedString}"
|
||||
Text="{Binding TimePlayedString}"
|
||||
IsVisible="{Binding HasPlayedPreviously}"
|
||||
TextAlignment="End"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
|
@ -12,8 +12,8 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
private static readonly Lazy<PlayabilityStatusConverter> _shared = new(() => new());
|
||||
public static PlayabilityStatusConverter Shared => _shared.Value;
|
||||
|
||||
public object Convert(object? value, Type _, object? __, CultureInfo ___) =>
|
||||
value.Cast<LocaleKeys>() switch
|
||||
public object Convert(object value, Type _, object __, CultureInfo ___)
|
||||
=> value.Cast<LocaleKeys>() switch
|
||||
{
|
||||
LocaleKeys.CompatibilityListNothing or
|
||||
LocaleKeys.CompatibilityListBoots or
|
||||
@ -22,7 +22,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
_ => Brushes.ForestGreen
|
||||
};
|
||||
|
||||
public object ConvertBack(object? value, Type _, object? __, CultureInfo ___)
|
||||
public object ConvertBack(object value, Type _, object __, CultureInfo ___)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
}
|
@ -63,6 +63,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
public static MiniCommand Create(Action callback) => new MiniCommand<object>(_ => callback());
|
||||
public static MiniCommand Create<TArg>(Action<TArg> callback) => new MiniCommand<TArg>(callback);
|
||||
public static MiniCommand CreateFromTask(Func<Task> callback) => new MiniCommand<object>(_ => callback());
|
||||
public static MiniCommand CreateFromTask<TArg>(Func<TArg, Task> callback) => new MiniCommand<TArg>(callback);
|
||||
|
||||
public abstract bool CanExecute(object parameter);
|
||||
public abstract void Execute(object parameter);
|
||||
|
@ -741,7 +741,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Applications.ToObservableChangeSet()
|
||||
.Filter(Filter)
|
||||
.Sort(GetComparer())
|
||||
.Bind(out _appsObservableList).AsObservableList();
|
||||
#pragma warning disable MVVMTK0034
|
||||
.Bind(out _appsObservableList)
|
||||
#pragma warning restore MVVMTK0034
|
||||
.AsObservableList();
|
||||
|
||||
OnPropertyChanged(nameof(AppsObservableList));
|
||||
}
|
||||
|
@ -56,7 +56,7 @@
|
||||
ToolTip.Tip="{ext:Locale LoadTitleUpdatesFromFolderTooltip}" />
|
||||
<MenuItem Header="{ext:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}" Icon="{ext:Icon mdi-launch}">
|
||||
<MenuItem
|
||||
Click="OpenMiiApplet"
|
||||
Name="MiiAppletMenuItem"
|
||||
Header="{ext:Locale MenuBarFileOpenAppletOpenMiiApplet}"
|
||||
Icon="{ext:Icon fa-solid fa-person}"
|
||||
ToolTip.Tip="{ext:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" />
|
||||
@ -72,7 +72,7 @@
|
||||
ToolTip.Tip="{ext:Locale OpenRyujinxLogsTooltip}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="CloseWindow"
|
||||
Name="CloseRyujinxMenuItem"
|
||||
Header="{ext:Locale MenuBarFileExit}"
|
||||
Icon="{ext:Icon fa-solid fa-xmark}"
|
||||
ToolTip.Tip="{ext:Locale ExitTooltip}" />
|
||||
@ -167,7 +167,7 @@
|
||||
Header="{ext:Locale MenuBarShowFileTypes}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Click="OpenSettings"
|
||||
Name="OpenSettingsMenuItem"
|
||||
Padding="0"
|
||||
Header="{ext:Locale MenuBarOptionsSettings}"
|
||||
Icon="{ext:Icon fa-solid fa-gear}"
|
||||
@ -210,21 +210,21 @@
|
||||
Header="{ext:Locale MenuBarActions}"
|
||||
IsEnabled="{Binding IsGameRunning}">
|
||||
<MenuItem
|
||||
Click="PauseEmulation_Click"
|
||||
Name="PauseEmulationMenuItem"
|
||||
Header="{ext:Locale MenuBarOptionsPauseEmulation}"
|
||||
Icon="{ext:Icon fa-solid fa-pause}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding !IsPaused}"
|
||||
IsVisible="{Binding !IsPaused}" />
|
||||
<MenuItem
|
||||
Click="ResumeEmulation_Click"
|
||||
Name="ResumeEmulationMenuItem"
|
||||
Header="{ext:Locale MenuBarOptionsResumeEmulation}"
|
||||
Icon="{ext:Icon fa-solid fa-play}"
|
||||
InputGesture="{Binding PauseKey}"
|
||||
IsEnabled="{Binding IsPaused}"
|
||||
IsVisible="{Binding IsPaused}" />
|
||||
<MenuItem
|
||||
Click="StopEmulation_Click"
|
||||
Name="StopEmulationMenuItem"
|
||||
Header="{ext:Locale MenuBarOptionsStopEmulation}"
|
||||
Icon="{ext:Icon fa-solid fa-stop}"
|
||||
InputGesture="Escape"
|
||||
@ -233,17 +233,15 @@
|
||||
<MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{ext:Locale MenuBarOptionsSimulateWakeUpMessage}" />
|
||||
<Separator />
|
||||
<MenuItem
|
||||
Name="ScanAmiiboMenuItem"
|
||||
Command="{Binding OpenAmiiboWindow}"
|
||||
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
|
||||
Click="OpenAmiiboWindow"
|
||||
Header="{ext:Locale MenuBarActionsScanAmiibo}"
|
||||
Icon="{ext:Icon mdi-cube-scan}"
|
||||
InputGesture="Ctrl + A"
|
||||
IsEnabled="{Binding IsAmiiboRequested}" />
|
||||
<MenuItem
|
||||
Name="ScanAmiiboMenuItemFromBin"
|
||||
Command="{Binding OpenBinFile}"
|
||||
AttachedToVisualTree="ScanBinAmiiboMenuItem_AttachedToVisualTree"
|
||||
Click="OpenBinFile"
|
||||
Header="{ext:Locale MenuBarActionsScanAmiiboBin}"
|
||||
Icon="{ext:Icon mdi-cube-scan}"
|
||||
IsVisible="{Binding CanScanAmiiboBinaries}"
|
||||
@ -262,7 +260,7 @@
|
||||
InputGesture="{Binding ShowUiKey}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
Click="OpenCheatManagerForCurrentApp"
|
||||
Name="CheatManagerMenuItem"
|
||||
Header="{ext:Locale GameListContextMenuManageCheat}"
|
||||
Icon="{ext:Icon fa-solid fa-code}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
@ -277,56 +275,55 @@
|
||||
<MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" Icon="{ext:Icon mdi-folder-cog}" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="{ext:Locale MenuBarToolsManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}">
|
||||
<MenuItem Header="{ext:Locale MenuBarToolsInstallFileTypes}" Click="InstallFileTypes_Click" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" />
|
||||
<MenuItem Header="{ext:Locale MenuBarToolsUninstallFileTypes}" Click="UninstallFileTypes_Click" IsEnabled="{Binding AreMimeTypesRegistered}" />
|
||||
<MenuItem Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBarToolsInstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" />
|
||||
<MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBarToolsUninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="{ext:Locale MenuBarToolsXCITrimmer}" IsEnabled="{Binding EnableNonGameRunningControls}" Click="OpenXCITrimmerWindow" Icon="{ext:Icon fa-solid fa-scissors}" />
|
||||
<MenuItem Name="XciTrimmerMenuItem" Header="{ext:Locale MenuBarToolsXCITrimmer}" IsEnabled="{Binding EnableNonGameRunningControls}" Icon="{ext:Icon fa-solid fa-scissors}" />
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarView}">
|
||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarViewWindow}">
|
||||
<MenuItem Header="{ext:Locale MenuBarViewWindow720}" Tag="1280 720" Click="ChangeWindowSize_Click" />
|
||||
<MenuItem Header="{ext:Locale MenuBarViewWindow1080}" Tag="1920 1080" Click="ChangeWindowSize_Click" />
|
||||
<MenuItem Header="{ext:Locale MenuBarViewWindow1440}" Tag="2560 1440" Click="ChangeWindowSize_Click" />
|
||||
<MenuItem Header="{ext:Locale MenuBarViewWindow2160}" Tag="3840 2160" Click="ChangeWindowSize_Click" />
|
||||
<MenuItem Name="WindowSize720PMenuItem" Header="{ext:Locale MenuBarViewWindow720}" CommandParameter="1280 720" />
|
||||
<MenuItem Name="WindowSize1080PMenuItem" Header="{ext:Locale MenuBarViewWindow1080}" CommandParameter="1920 1080" />
|
||||
<MenuItem Name="WindowSize1440PMenuItem" Header="{ext:Locale MenuBarViewWindow1440}" CommandParameter="2560 1440" />
|
||||
<MenuItem Name="WindowSize2160PMenuItem" Header="{ext:Locale MenuBarViewWindow2160}" CommandParameter="3840 2160" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelp}">
|
||||
<MenuItem
|
||||
Click="OpenAboutWindow"
|
||||
Name="AboutWindowMenuItem"
|
||||
Header="{ext:Locale MenuBarHelpAbout}"
|
||||
Icon="{ext:Icon fa-solid fa-circle-info}"
|
||||
ToolTip.Tip="{ext:Locale OpenAboutTooltip}" />
|
||||
<MenuItem
|
||||
Name="UpdateMenuItem"
|
||||
IsEnabled="{Binding CanUpdate}"
|
||||
Click="CheckForUpdates"
|
||||
Header="{ext:Locale MenuBarHelpCheckForUpdates}"
|
||||
Icon="{ext:Icon mdi-update}"
|
||||
ToolTip.Tip="{ext:Locale CheckUpdatesTooltip}" />
|
||||
<MenuItem
|
||||
Click="OpenCompatibilityList"
|
||||
Name="CompatibilityListMenuItem"
|
||||
Header="{ext:Locale CompatibilityListOpen}"
|
||||
Icon="{ext:Icon mdi-gamepad}"/>
|
||||
<Separator />
|
||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelpFaqAndGuides}" Icon="{ext:Icon fa-solid fa-question}" >
|
||||
<MenuItem
|
||||
Click="MenuItem_OnClick"
|
||||
Name="FaqMenuItem"
|
||||
Header="{ext:Locale MenuBarHelpFaq}"
|
||||
Icon="{ext:Icon fa-github}"
|
||||
Tag="https://github.com/GreemDev/Ryujinx/wiki/FAQ-and-Troubleshooting"
|
||||
CommandParameter="https://github.com/GreemDev/Ryujinx/wiki/FAQ-and-Troubleshooting"
|
||||
ToolTip.Tip="{ext:Locale MenuBarHelpFaqTooltip}" />
|
||||
<MenuItem
|
||||
Click="MenuItem_OnClick"
|
||||
Name="SetupGuideMenuItem"
|
||||
Header="{ext:Locale MenuBarHelpSetup}"
|
||||
Icon="{ext:Icon fa-github}"
|
||||
Tag="https://github.com/GreemDev/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide"
|
||||
CommandParameter="https://github.com/GreemDev/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide"
|
||||
ToolTip.Tip="{ext:Locale MenuBarHelpSetupTooltip}" />
|
||||
<MenuItem
|
||||
Click="MenuItem_OnClick"
|
||||
Name="LdnGuideMenuItem"
|
||||
Header="{ext:Locale MenuBarHelpMultiplayer}"
|
||||
Icon="{ext:Icon fa-github}"
|
||||
Tag="https://github.com/GreemDev/Ryujinx/wiki/Multiplayer%E2%80%90(LDN%E2%80%90Local%E2%80%90Wireless)%E2%80%90Guide"
|
||||
CommandParameter="https://github.com/GreemDev/Ryujinx/wiki/Multiplayer%E2%80%90(LDN%E2%80%90Local%E2%80%90Wireless)%E2%80%90Guide"
|
||||
ToolTip.Tip="{ext:Locale MenuBarHelpMultiplayerTooltip}" />
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
|
@ -1,7 +1,8 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Gommon;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
@ -17,6 +18,7 @@ using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.Main
|
||||
{
|
||||
@ -34,9 +36,37 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
|
||||
ToggleFileTypesMenuItem.ItemsSource = GenerateToggleFileTypeItems();
|
||||
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
|
||||
|
||||
MiiAppletMenuItem.Command = new AsyncRelayCommand(OpenMiiApplet);
|
||||
CloseRyujinxMenuItem.Command = new RelayCommand(CloseWindow);
|
||||
OpenSettingsMenuItem.Command = new AsyncRelayCommand(OpenSettings);
|
||||
PauseEmulationMenuItem.Command = new RelayCommand(() => ViewModel.AppHost?.Pause());
|
||||
ResumeEmulationMenuItem.Command = new RelayCommand(() => ViewModel.AppHost?.Resume());
|
||||
StopEmulationMenuItem.Command = new AsyncRelayCommand(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
|
||||
CheatManagerMenuItem.Command = new AsyncRelayCommand(OpenCheatManagerForCurrentApp);
|
||||
InstallFileTypesMenuItem.Command = new AsyncRelayCommand(InstallFileTypes);
|
||||
UninstallFileTypesMenuItem.Command = new AsyncRelayCommand(UninstallFileTypes);
|
||||
XciTrimmerMenuItem.Command = new AsyncRelayCommand(() => XCITrimmerWindow.Show(ViewModel));
|
||||
AboutWindowMenuItem.Command = new AsyncRelayCommand(AboutWindow.Show);
|
||||
CompatibilityListMenuItem.Command = new AsyncRelayCommand(CompatibilityList.Show);
|
||||
|
||||
UpdateMenuItem.Command = new AsyncRelayCommand(async () =>
|
||||
{
|
||||
if (Updater.CanUpdate(true))
|
||||
await Updater.BeginUpdateAsync(true);
|
||||
});
|
||||
|
||||
FaqMenuItem.Command =
|
||||
SetupGuideMenuItem.Command =
|
||||
LdnGuideMenuItem.Command = new RelayCommand<string>(OpenHelper.OpenUrl);
|
||||
|
||||
WindowSize720PMenuItem.Command =
|
||||
WindowSize1080PMenuItem.Command =
|
||||
WindowSize1440PMenuItem.Command =
|
||||
WindowSize2160PMenuItem.Command = new RelayCommand<string>(ChangeWindowSize);
|
||||
}
|
||||
|
||||
private CheckBox[] GenerateToggleFileTypeItems() =>
|
||||
private IEnumerable<CheckBox> GenerateToggleFileTypeItems() =>
|
||||
Enum.GetValues<FileTypes>()
|
||||
.Select(it => (FileName: Enum.GetName(it)!, FileType: it))
|
||||
.Select(it =>
|
||||
@ -46,15 +76,13 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
IsChecked = it.FileType.GetConfigValue(ConfigurationState.Instance.UI.ShownFileTypes),
|
||||
Command = MiniCommand.Create(() => Window.ToggleFileType(it.FileName))
|
||||
}
|
||||
).ToArray();
|
||||
);
|
||||
|
||||
private static MenuItem[] GenerateLanguageMenuItems()
|
||||
private static IEnumerable<MenuItem> GenerateLanguageMenuItems()
|
||||
{
|
||||
List<MenuItem> menuItems = new();
|
||||
const string LocalePath = "Ryujinx/Assets/locales.json";
|
||||
|
||||
string localePath = "Ryujinx/Assets/locales.json";
|
||||
|
||||
string languageJson = EmbeddedResources.ReadAllText(localePath);
|
||||
string languageJson = EmbeddedResources.ReadAllText(LocalePath);
|
||||
|
||||
LocalesJson locales = JsonHelper.Deserialize(languageJson, LocalesJsonContext.Default.LocalesJson);
|
||||
|
||||
@ -69,20 +97,23 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
}
|
||||
else
|
||||
{
|
||||
languageName = locales.Locales[index].Translations[language] == "" ? language : locales.Locales[index].Translations[language];
|
||||
string tr = locales.Locales[index].Translations[language];
|
||||
languageName = string.IsNullOrEmpty(tr)
|
||||
? language
|
||||
: tr;
|
||||
}
|
||||
|
||||
MenuItem menuItem = new()
|
||||
{
|
||||
Padding = new Thickness(10, 0, 0, 0),
|
||||
Header = " " + languageName,
|
||||
Padding = new Thickness(15, 0, 0, 0),
|
||||
Margin = new Thickness(3, 0, 3, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
Header = languageName,
|
||||
Command = MiniCommand.Create(() => MainWindowViewModel.ChangeLanguage(language))
|
||||
};
|
||||
|
||||
menuItems.Add(menuItem);
|
||||
yield return menuItem;
|
||||
}
|
||||
|
||||
return menuItems.ToArray();
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
@ -96,22 +127,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
}
|
||||
}
|
||||
|
||||
private async void StopEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await ViewModel.AppHost?.ShowExitPrompt().OrCompleted()!;
|
||||
}
|
||||
|
||||
private void PauseEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.AppHost?.Pause();
|
||||
}
|
||||
|
||||
private void ResumeEmulation_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.AppHost?.Resume();
|
||||
}
|
||||
|
||||
public async void OpenSettings(object sender, RoutedEventArgs e)
|
||||
public async Task OpenSettings()
|
||||
{
|
||||
Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager);
|
||||
|
||||
@ -124,7 +140,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
|
||||
public static readonly AppletMetadata MiiApplet = new("miiEdit", 0x0100000000001009);
|
||||
|
||||
public async void OpenMiiApplet(object sender, RoutedEventArgs e)
|
||||
public async Task OpenMiiApplet()
|
||||
{
|
||||
if (MiiApplet.CanStart(ViewModel.ContentManager, out var appData, out var nacpData))
|
||||
{
|
||||
@ -132,13 +148,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
}
|
||||
}
|
||||
|
||||
public async void OpenAmiiboWindow(object sender, RoutedEventArgs e)
|
||||
=> await ViewModel.OpenAmiiboWindow();
|
||||
|
||||
public async void OpenBinFile(object sender, RoutedEventArgs e)
|
||||
=> await ViewModel.OpenBinFile();
|
||||
|
||||
public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e)
|
||||
public async Task OpenCheatManagerForCurrentApp()
|
||||
{
|
||||
if (!ViewModel.IsGameRunning)
|
||||
return;
|
||||
@ -166,7 +176,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
ViewModel.IsAmiiboBinRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasAmiiboKeyFile;
|
||||
}
|
||||
|
||||
private async void InstallFileTypes_Click(object sender, RoutedEventArgs e)
|
||||
private async Task InstallFileTypes()
|
||||
{
|
||||
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();
|
||||
if (ViewModel.AreMimeTypesRegistered)
|
||||
@ -175,7 +185,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]);
|
||||
}
|
||||
|
||||
private async void UninstallFileTypes_Click(object sender, RoutedEventArgs e)
|
||||
private async Task UninstallFileTypes()
|
||||
{
|
||||
ViewModel.AreMimeTypesRegistered = !FileAssociationHelper.Uninstall();
|
||||
if (!ViewModel.AreMimeTypesRegistered)
|
||||
@ -184,11 +194,8 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesErrorMessage]);
|
||||
}
|
||||
|
||||
private async void ChangeWindowSize_Click(object sender, RoutedEventArgs e)
|
||||
private void ChangeWindowSize(string resolution)
|
||||
{
|
||||
if (sender is not MenuItem { Tag: string resolution })
|
||||
return;
|
||||
|
||||
(int resolutionWidth, int resolutionHeight) = resolution.Split(' ', 2)
|
||||
.Into(parts =>
|
||||
(int.Parse(parts[0]), int.Parse(parts[1]))
|
||||
@ -201,7 +208,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor);
|
||||
double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor);
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ViewModel.WindowState = WindowState.Normal;
|
||||
|
||||
@ -209,24 +216,7 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
});
|
||||
}
|
||||
|
||||
public async void CheckForUpdates(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Updater.CanUpdate(true))
|
||||
await Updater.BeginUpdateAsync(true);
|
||||
}
|
||||
|
||||
private void MenuItem_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem { Tag: string url })
|
||||
OpenHelper.OpenUrl(url);
|
||||
}
|
||||
|
||||
public async void OpenXCITrimmerWindow(object sender, RoutedEventArgs e) => await XCITrimmerWindow.Show(ViewModel);
|
||||
|
||||
public async void OpenAboutWindow(object sender, RoutedEventArgs e) => await AboutWindow.Show();
|
||||
|
||||
public void CloseWindow(object sender, RoutedEventArgs e) => Window.Close();
|
||||
|
||||
private async void OpenCompatibilityList(object sender, RoutedEventArgs e) => await CompatibilityContentDialog.Show();
|
||||
public void CloseWindow() => Window.Close();
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using FluentAvalonia.Core;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
@ -23,6 +25,11 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
|
||||
InitializeComponent();
|
||||
Load();
|
||||
|
||||
#if DEBUG
|
||||
this.AttachDevTools(new KeyGesture(Key.F12, KeyModifiers.Alt));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
public SettingsWindow()
|
||||
|
@ -37,6 +37,8 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
|
||||
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
|
||||
|
||||
public bool HasPlayedPreviously => TimePlayedString != string.Empty;
|
||||
|
||||
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
|
||||
|
||||
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
||||
|
@ -32,29 +32,27 @@ namespace Ryujinx.Ava.Utilities
|
||||
|
||||
public string GetContentPath(ContentManager contentManager)
|
||||
=> (contentManager ?? _contentManager)
|
||||
.GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program);
|
||||
?.GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program);
|
||||
|
||||
public bool CanStart(ContentManager contentManager, out ApplicationData appData,
|
||||
out BlitStruct<ApplicationControlProperty> appControl)
|
||||
{
|
||||
contentManager ??= _contentManager;
|
||||
if (contentManager == null)
|
||||
{
|
||||
appData = null;
|
||||
appControl = new BlitStruct<ApplicationControlProperty>(0);
|
||||
return false;
|
||||
}
|
||||
if (contentManager == null)
|
||||
goto BadData;
|
||||
|
||||
string contentPath = GetContentPath(contentManager);
|
||||
if (string.IsNullOrEmpty(contentPath))
|
||||
goto BadData;
|
||||
|
||||
appData = new() { Name = Name, Id = ProgramId, Path = GetContentPath(contentManager) };
|
||||
|
||||
if (string.IsNullOrEmpty(appData.Path))
|
||||
{
|
||||
appControl = new BlitStruct<ApplicationControlProperty>(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
appControl = StructHelpers.CreateCustomNacpData(Name, Version);
|
||||
return true;
|
||||
|
||||
BadData:
|
||||
appData = null;
|
||||
appControl = new BlitStruct<ApplicationControlProperty>(0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
<ui:ContentDialog 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:local="using:Ryujinx.Ava.Utilities.Compat"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
|
||||
x:Class="Ryujinx.Ava.Utilities.Compat.CompatibilityContentDialog"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400"
|
||||
CloseButtonText="{ext:Locale SettingsButtonClose}"
|
||||
DefaultButton="Close"
|
||||
x:DataType="local:CompatibilityViewModel">
|
||||
<ui:ContentDialog.DataContext>
|
||||
<local:CompatibilityViewModel/>
|
||||
</ui:ContentDialog.DataContext>
|
||||
<ui:ContentDialog.Resources>
|
||||
<x:Double x:Key="ContentDialogMaxWidth">900</x:Double>
|
||||
</ui:ContentDialog.Resources>
|
||||
</ui:ContentDialog>
|
||||
|
@ -1,37 +0,0 @@
|
||||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
public partial class CompatibilityContentDialog : ContentDialog
|
||||
{
|
||||
protected override Type StyleKeyOverride => typeof(ContentDialog);
|
||||
|
||||
public static async Task Show()
|
||||
{
|
||||
await CompatibilityHelper.InitAsync();
|
||||
|
||||
CompatibilityContentDialog contentDialog = new()
|
||||
{
|
||||
Content = new CompatibilityList { DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary) }
|
||||
};
|
||||
|
||||
Style closeButton = new(x => x.Name("CloseButton"));
|
||||
closeButton.Setters.Add(new Setter(WidthProperty, 80d));
|
||||
|
||||
Style closeButtonParent = new(x => x.Name("CommandSpace"));
|
||||
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, Avalonia.Layout.HorizontalAlignment.Right));
|
||||
|
||||
contentDialog.Styles.Add(closeButton);
|
||||
contentDialog.Styles.Add(closeButtonParent);
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
}
|
||||
|
||||
public CompatibilityContentDialog() => InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +1,66 @@
|
||||
using Gommon;
|
||||
using Humanizer;
|
||||
using nietras.SeparatedValues;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
public struct ColumnIndices(Func<ReadOnlySpan<char>, int> getIndex)
|
||||
{
|
||||
public const string TitleIdCol = "\"title_id\"";
|
||||
public const string GameNameCol = "\"game_name\"";
|
||||
public const string LabelsCol = "\"labels\"";
|
||||
public const string StatusCol = "\"status\"";
|
||||
public const string LastUpdatedCol = "\"last_updated\"";
|
||||
|
||||
public readonly int TitleId = getIndex(TitleIdCol);
|
||||
public readonly int GameName = getIndex(GameNameCol);
|
||||
public readonly int Labels = getIndex(LabelsCol);
|
||||
public readonly int Status = getIndex(StatusCol);
|
||||
public readonly int LastUpdated = getIndex(LastUpdatedCol);
|
||||
}
|
||||
|
||||
public class CompatibilityCsv
|
||||
{
|
||||
public static CompatibilityCsv Shared { get; set; }
|
||||
|
||||
public CompatibilityCsv(SepReader reader)
|
||||
static CompatibilityCsv()
|
||||
{
|
||||
var entries = new List<CompatibilityEntry>();
|
||||
using Stream csvStream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("RyujinxGameCompatibilityList")!;
|
||||
csvStream.Position = 0;
|
||||
|
||||
foreach (var row in reader)
|
||||
{
|
||||
entries.Add(new CompatibilityEntry(reader.Header, row));
|
||||
}
|
||||
using SepReader reader = Sep.Reader().From(csvStream);
|
||||
ColumnIndices columnIndices = new(reader.Header.IndexOf);
|
||||
|
||||
Entries = entries.Where(x => x.Status != null)
|
||||
.OrderBy(it => it.GameName).ToArray();
|
||||
Entries = reader
|
||||
.Enumerate(row => new CompatibilityEntry(ref columnIndices, row))
|
||||
.OrderBy(it => it.GameName)
|
||||
.ToArray();
|
||||
|
||||
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatCsv");
|
||||
}
|
||||
|
||||
public CompatibilityEntry[] Entries { get; }
|
||||
public static CompatibilityEntry[] Entries { get; private set; }
|
||||
}
|
||||
|
||||
public class CompatibilityEntry
|
||||
{
|
||||
public CompatibilityEntry(SepReaderHeader header, SepReader.Row row)
|
||||
public CompatibilityEntry(ref ColumnIndices indices, SepReader.Row row)
|
||||
{
|
||||
IssueNumber = row[header.IndexOf("issue_number")].Parse<int>();
|
||||
|
||||
var titleIdRow = row[header.IndexOf("extracted_game_id")].ToString();
|
||||
string titleIdRow = ColStr(row[indices.TitleId]);
|
||||
TitleId = !string.IsNullOrEmpty(titleIdRow)
|
||||
? titleIdRow
|
||||
: default(Optional<string>);
|
||||
|
||||
GameName = ColStr(row[indices.GameName]).Trim().Trim('"');
|
||||
|
||||
var issueTitleRow = row[header.IndexOf("issue_title")].ToString();
|
||||
if (TitleId.HasValue)
|
||||
issueTitleRow = issueTitleRow.ReplaceIgnoreCase($" - {TitleId}", string.Empty);
|
||||
|
||||
GameName = issueTitleRow.Trim().Trim('"');
|
||||
|
||||
IssueLabels = row[header.IndexOf("issue_labels")].ToString().Split(';');
|
||||
Status = row[header.IndexOf("extracted_status")].ToString().ToLower() switch
|
||||
Labels = ColStr(row[indices.Labels]).Split(';');
|
||||
Status = ColStr(row[indices.Status]).ToLower() switch
|
||||
{
|
||||
"playable" => LocaleKeys.CompatibilityListPlayable,
|
||||
"ingame" => LocaleKeys.CompatibilityListIngame,
|
||||
@ -56,39 +70,42 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (row[header.IndexOf("last_event_date")].TryParse<DateTime>(out var dt))
|
||||
LastEvent = dt;
|
||||
if (DateTime.TryParse(ColStr(row[indices.LastUpdated]), out var dt))
|
||||
LastUpdated = dt;
|
||||
|
||||
if (row[header.IndexOf("events_count")].TryParse<int>(out var eventsCount))
|
||||
EventCount = eventsCount;
|
||||
return;
|
||||
|
||||
string ColStr(SepReader.Col col) => col.ToString().Trim('"');
|
||||
}
|
||||
|
||||
public int IssueNumber { get; }
|
||||
|
||||
public string GameName { get; }
|
||||
public Optional<string> TitleId { get; }
|
||||
public string[] IssueLabels { get; }
|
||||
public string[] Labels { get; }
|
||||
public LocaleKeys? Status { get; }
|
||||
public DateTime LastEvent { get; }
|
||||
public int EventCount { get; }
|
||||
public DateTime LastUpdated { get; }
|
||||
|
||||
public string LocalizedLastUpdated =>
|
||||
LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
|
||||
|
||||
public string LocalizedStatus => LocaleManager.Instance[Status!.Value];
|
||||
public string FormattedTitleId => TitleId.OrElse(new string(' ', 16));
|
||||
public string FormattedTitleId => TitleId
|
||||
.OrElse(new string(' ', 16));
|
||||
|
||||
public string FormattedIssueLabels => IssueLabels
|
||||
public string FormattedIssueLabels => Labels
|
||||
.Where(it => !it.StartsWithIgnoreCase("status"))
|
||||
.Select(FormatLabelName)
|
||||
.JoinToString(", ");
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder("CompatibilityEntry: {");
|
||||
sb.Append($"{nameof(IssueNumber)}={IssueNumber}, ");
|
||||
StringBuilder sb = new("CompatibilityEntry: {");
|
||||
sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
|
||||
sb.Append($"{nameof(TitleId)}={TitleId}, ");
|
||||
sb.Append($"{nameof(IssueLabels)}=\"{IssueLabels}\", ");
|
||||
sb.Append($"{nameof(Labels)}={
|
||||
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
|
||||
}, ");
|
||||
sb.Append($"{nameof(Status)}=\"{Status}\", ");
|
||||
sb.Append($"{nameof(LastEvent)}=\"{LastEvent}\", ");
|
||||
sb.Append($"{nameof(EventCount)}={EventCount}");
|
||||
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
|
||||
sb.Append('}');
|
||||
|
||||
return sb.ToString();
|
||||
@ -144,8 +161,8 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
if (value == string.Empty)
|
||||
return string.Empty;
|
||||
|
||||
var firstChar = value[0];
|
||||
var rest = value[1..];
|
||||
char firstChar = value[0];
|
||||
string rest = value[1..];
|
||||
|
||||
return $"{char.ToUpper(firstChar)}{rest}";
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
using Gommon;
|
||||
using nietras.SeparatedValues;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
public static class CompatibilityHelper
|
||||
{
|
||||
private static readonly string _downloadUrl =
|
||||
"https://gist.githubusercontent.com/ezhevita/b41ed3bf64d0cc01269cab036e884f3d/raw/002b1a1c1a5f7a83276625e8c479c987a5f5b722/Ryujinx%2520Games%2520List%2520Compatibility.csv";
|
||||
|
||||
private static readonly FilePath _compatCsvPath = new FilePath(AppDataManager.BaseDirPath) / "system" / "compatibility.csv";
|
||||
|
||||
public static async Task<SepReader> DownloadAsync()
|
||||
{
|
||||
if (_compatCsvPath.ExistsAsFile)
|
||||
return Sep.Reader().FromFile(_compatCsvPath.Path);
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
var compatCsv = await httpClient.GetStringAsync(_downloadUrl);
|
||||
_compatCsvPath.WriteAllText(compatCsv);
|
||||
return Sep.Reader().FromText(compatCsv);
|
||||
}
|
||||
|
||||
public static async Task InitAsync()
|
||||
{
|
||||
CompatibilityCsv.Shared = new CompatibilityCsv(await DownloadAsync());
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
xmlns:local="using:Ryujinx.Ava.Utilities.Compat"
|
||||
xmlns:helpers="using:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Ryujinx.Ava.Utilities.Compat.CompatibilityList"
|
||||
@ -11,24 +12,46 @@
|
||||
<UserControl.DataContext>
|
||||
<local:CompatibilityViewModel />
|
||||
</UserControl.DataContext>
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto">
|
||||
<Grid RowDefinitions="*,Auto,*">
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Center"
|
||||
ColumnDefinitions="Auto,*"
|
||||
Margin="0 0 0 10">
|
||||
<ui:FontIcon
|
||||
Grid.Column="0"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
|
||||
Glyph="{helpers:GlyphValueConverter Important}" />
|
||||
<!-- NOTE: aligning to bottom for better visual alignment with glyph -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="5, 0, 0, 0"
|
||||
FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Text="{ext:Locale CompatibilityListWarning}" />
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto">
|
||||
<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>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<ListBox Margin="0,5, 0, 0"
|
||||
Background="Transparent"
|
||||
ItemsSource="{Binding CurrentEntries}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:CompatibilityEntry}">
|
||||
<Grid Width="750" ColumnDefinitions="Auto,Auto,Auto,*"
|
||||
Margin="5">
|
||||
<Grid Width="750"
|
||||
Margin="5"
|
||||
ColumnDefinitions="Auto,Auto,Auto,*"
|
||||
Background="Transparent"
|
||||
ToolTip.Tip="{Binding LocalizedLastUpdated}">
|
||||
<TextBlock Grid.Column="0"
|
||||
FontFamily="{StaticResource JetBrainsMono}"
|
||||
Text="{Binding GameName}"
|
||||
Width="333"
|
||||
Width="320"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Width="135"
|
||||
@ -39,14 +62,12 @@
|
||||
<TextBlock Grid.Column="2"
|
||||
Padding="7, 0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource JetBrainsMono}"
|
||||
Text="{Binding LocalizedStatus}"
|
||||
Width="85"
|
||||
Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource JetBrainsMono}"
|
||||
Text="{Binding FormattedIssueLabels}"
|
||||
TextWrapping="WrapWithOverflow" />
|
||||
</Grid>
|
||||
|
@ -1,15 +1,48 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Styling;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using nietras.SeparatedValues;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
public partial class CompatibilityList : UserControl
|
||||
{
|
||||
public static async Task Show()
|
||||
{
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
PrimaryButtonText = string.Empty,
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
Content = new CompatibilityList
|
||||
{
|
||||
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary)
|
||||
}
|
||||
};
|
||||
|
||||
Style closeButton = new(x => x.Name("CloseButton"));
|
||||
closeButton.Setters.Add(new Setter(WidthProperty, 80d));
|
||||
|
||||
Style closeButtonParent = new(x => x.Name("CommandSpace"));
|
||||
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, Avalonia.Layout.HorizontalAlignment.Right));
|
||||
|
||||
contentDialog.Styles.Add(closeButton);
|
||||
contentDialog.Styles.Add(closeButtonParent);
|
||||
|
||||
await ContentDialogHelper.ShowAsync(contentDialog);
|
||||
}
|
||||
|
||||
public CompatibilityList()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void TextBox_OnTextChanged(object? sender, TextChangedEventArgs e)
|
||||
private void TextBox_OnTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (DataContext is not CompatibilityViewModel cvm)
|
||||
return;
|
||||
|
@ -11,14 +11,13 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
[ObservableProperty] private bool _onlyShowOwnedGames = true;
|
||||
|
||||
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityCsv.Shared.Entries;
|
||||
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityCsv.Entries;
|
||||
private readonly string[] _ownedGameTitleIds = [];
|
||||
private readonly ApplicationLibrary _appLibrary;
|
||||
|
||||
public IEnumerable<CompatibilityEntry> CurrentEntries => OnlyShowOwnedGames
|
||||
? _currentEntries.Where(x =>
|
||||
x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid))
|
||||
|| _appLibrary.Applications.Items.Any(a => a.Name.EqualsIgnoreCase(x.GameName)))
|
||||
x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid)))
|
||||
: _currentEntries;
|
||||
|
||||
public CompatibilityViewModel() {}
|
||||
@ -39,11 +38,11 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
SetEntries(CompatibilityCsv.Shared.Entries);
|
||||
SetEntries(CompatibilityCsv.Entries);
|
||||
return;
|
||||
}
|
||||
|
||||
SetEntries(CompatibilityCsv.Shared.Entries.Where(x =>
|
||||
SetEntries(CompatibilityCsv.Entries.Where(x =>
|
||||
x.GameName.ContainsIgnoreCase(searchTerm)
|
||||
|| x.TitleId.Check(tid => tid.ContainsIgnoreCase(searchTerm))));
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
using Humanizer;
|
||||
using Humanizer.Localisation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
@ -31,7 +33,7 @@ namespace Ryujinx.Ava.Utilities
|
||||
Gigabytes = 9,
|
||||
Terabytes = 10,
|
||||
Petabytes = 11,
|
||||
Exabytes = 12,
|
||||
Exabytes = 12
|
||||
}
|
||||
|
||||
private const double SizeBase10 = 1000;
|
||||
@ -48,22 +50,24 @@ namespace Ryujinx.Ava.Utilities
|
||||
public static string FormatTimeSpan(TimeSpan? timeSpan)
|
||||
{
|
||||
if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
|
||||
{
|
||||
// Game was never played
|
||||
return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
return string.Empty;
|
||||
|
||||
if (timeSpan.Value.TotalSeconds < 60)
|
||||
return timeSpan.Value.Humanize(1,
|
||||
countEmptyUnits: false,
|
||||
maxUnit: TimeUnit.Second,
|
||||
minUnit: TimeUnit.Second);
|
||||
|
||||
if (timeSpan.Value.TotalDays < 1)
|
||||
{
|
||||
// Game was played for less than a day
|
||||
return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// Game was played for more than a day
|
||||
TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
|
||||
string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
|
||||
|
||||
return $"{timeSpan.Value.Days}d, {onlyTimeString}";
|
||||
if (timeSpan.Value.TotalMinutes < 60)
|
||||
return timeSpan.Value.Humanize(1,
|
||||
countEmptyUnits: false,
|
||||
maxUnit: TimeUnit.Minute,
|
||||
minUnit: TimeUnit.Minute);
|
||||
|
||||
return timeSpan.Value.Humanize(1,
|
||||
countEmptyUnits: false,
|
||||
maxUnit: TimeUnit.Hour,
|
||||
minUnit: TimeUnit.Hour);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
Reference in New Issue
Block a user