Compare commits

...

33 Commits

Author SHA1 Message Date
cc95e80ee9 misc: chore: Move converters into a directory in Helpers. Namespace unchanged 2025-01-10 20:24:53 -06:00
d75ce52bd4 UI: Show play time in one time unit, maxing out at hours. 2025-01-10 20:23:47 -06:00
4a4ea557de UI: compat: show last updated date on entry hover 2025-01-10 01:43:34 -06:00
33f42adb11 Merge remote-tracking branch 'origin/master' 2025-01-09 22:09:01 -06:00
918ec1bde3 cores rework (#505)
This PR changes the core count to be defined in the device instead of
being a const value.
This is mostly a change for future features I want to implement and
should not impact any functionality.
The console will now log the range of cores requested from the
application, and for now, if the requested range is not 0 to 2 (the 3
cores used for application emulation), it will give an error message
which tells the user to contact me on discord. I'm doing this because
I'm interested in finding applications/games that don't use 3 cores and
the error will be removed in the future once I've gotten enough data.
2025-01-09 21:43:18 -06:00
cca429d46a misc: chore: restore not enable 2025-01-09 21:42:54 -06:00
845c86f545 misc: chore: cleanup AppletMetadata.CanStart 2025-01-09 21:14:35 -06:00
27993b789f misc: chore: fix some compile warnings 2025-01-09 20:23:26 -06:00
bdd890cf6f UI: logger function name 2025-01-09 19:48:11 -06:00
c5574b41a1 UI: collapse LoadFromStream into static ctor
pass the index get delegate to the struct instead of the entire header
2025-01-09 19:44:24 -06:00
292e27f0da UI: dispose CSV reader when done + use explicit types 2025-01-09 19:24:48 -06:00
606e149bd3 UI: Create a ColumnIndices struct and pass it by reference to the row ctor instead of recomputing the column index for every column on every row 2025-01-09 18:48:15 -06:00
a8c3407d11 missing JP title id 2025-01-09 14:25:16 -06:00
daa8168985 docs: compat: the final title IDs i could find 2025-01-09 14:03:37 -06:00
f580521e99 Update game data in the compatibility database (#507)
The entries were matched with the game database from
https://github.com/blawar/titledb/blob/master/US.en.json, allowing to
fill missing title IDs and fix game names.
2025-01-09 13:18:27 -06:00
2226521f6c docs: compat: remove quotes around everything but game titles 2025-01-08 12:36:26 -06:00
384416953d docs: compat: list title ID column first 2025-01-08 12:30:13 -06:00
1343fabe41 docs: compat: some more missing title ids 2025-01-08 12:20:20 -06:00
1e52af5e29 docs: compat: Multiple big changes:
Sort alphabetically,
Remove title IDs in "issue_title" column,
and remove all entries without a playability status.
2025-01-07 20:17:09 -06:00
672f5df0f9 docs: compat: Remove issue_number & events_count columns
That's mostly for archival purposes; we don't need it.
2025-01-07 18:49:04 -06:00
804d9c1efe docs: compat: remove invalid dupe 2025-01-07 06:03:35 -06:00
9270b35648 no. 2025-01-07 05:53:31 -06:00
5a6d01db3c docs: compat: trine 4 -> nothing
added Soul Reaver 1 & 2
2025-01-07 05:50:30 -06:00
ef9c1416ec UI: compat: Only use monospaced font for title ID 2025-01-07 04:49:20 -06:00
5efa7d5dfa UI: compat: remove custom ContentDialog derived type 2025-01-07 04:37:36 -06:00
a82569d615 docs: compat: LEGO Horizon Adventures 2025-01-07 04:28:10 -06:00
ed5832ca73 docs: compat: Add new releases to the end of the file 2025-01-07 04:21:33 -06:00
574aa9ff9c add a couple missing title IDs 2025-01-07 04:21:08 -06:00
8a29428de2 docs: compat: update hogwarts legacy compat 2025-01-07 03:57:13 -06:00
f4272b05fa UI: Compat list disclaimer 2025-01-07 03:53:10 -06:00
d8265f7772 Embed compatibility list into executable
instead of downloading

Co-Authored-By: Vita Chumakova <me@ezhevita.dev>
2025-01-07 03:37:07 -06:00
259526430c UI: Properly space language menu items instead of prepending a space to the language name 2025-01-07 00:36:22 -06:00
b5fafb6394 UI: stop using async voids in MainMenuBarView; use RelayCommands 2025-01-06 23:52:20 -06:00
42 changed files with 3769 additions and 300 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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,

View File

@ -63,6 +63,7 @@ namespace Ryujinx.HLE.HOS.Kernel
TickSource = tickSource;
Device = device;
Memory = memory;
KScheduler.CpuCoresCount = device.CpuCoresCount;
Running = true;

View File

@ -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);

View File

@ -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)
{

View File

@ -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;
}

View File

@ -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)
{

View File

@ -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();

View File

@ -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);

View File

@ -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; }

View File

@ -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>

View File

@ -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": {

View File

@ -1,4 +1,4 @@
using DiscordRPC;
using DiscordRPC;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.SDL2;
using Ryujinx.Ava;

View File

@ -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>

View File

@ -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

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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));
}

View File

@ -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-&amp;-Configuration-Guide"
CommandParameter="https://github.com/GreemDev/Ryujinx/wiki/Ryujinx-Setup-&amp;-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>

View File

@ -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();
}
}

View File

@ -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()

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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}";
}

View File

@ -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());
}
}
}

View File

@ -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>

View File

@ -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;

View File

@ -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))));
}

View File

@ -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>