Compare commits

...

42 Commits

Author SHA1 Message Date
0dd789e8a5 misc: chore: remove redundant trimming on CompatibilityEntry.GameName init 2025-01-11 01:26:34 -06:00
4e0aafd005 docs: compat: Trim redundant/duplicate information to save space 2025-01-11 01:18:10 -06:00
c5091f499e docs: compat: The House of the Dead: Remake Playable 2025-01-11 00:38:32 -06:00
41c8fd8194 misc: chore: lol this field was misspelled 2025-01-10 23:23:53 -06:00
d4a7ee25ea misc: chore: use ObservableProperty on input view models 2025-01-10 23:23:05 -06:00
3141c560fb misc: chore: remove sender parameter from LdnGameData receieved event 2025-01-10 23:15:55 -06:00
de341b285b misc: use ObservableProperty on HotkeyConfig fields 2025-01-10 23:15:37 -06:00
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
323c356d9c UI: Widen compatibility list, and make search box take up all space horizontally 2025-01-06 22:03:39 -06:00
30b22ce6ba UI: fix nullref 2025-01-06 08:02:37 -06:00
48 changed files with 3819 additions and 573 deletions

View File

@ -42,7 +42,7 @@
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" /> <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.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" /> <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="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.6.0" /> <PackageVersion Include="Sep" Version="0.6.0" />
<PackageVersion Include="shaderc.net" Version="0.1.0" /> <PackageVersion Include="shaderc.net" Version="0.1.0" />

3423
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); ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
uint[] defaultCapabilities = { uint[] defaultCapabilities = {
0x030363F7, (((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u,
0x1FFFFFCF, 0x1FFFFFCF,
0x207FFFEF, 0x207FFFEF,
0x47E0060F, 0x47E0060F,

View File

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

View File

@ -37,7 +37,7 @@ namespace Ryujinx.HLE.HOS.Kernel
return result; return result;
} }
process.DefaultCpuCore = 3; process.DefaultCpuCore = KScheduler.CpuCoresCount - 1;
context.Processes.TryAdd(process.Pid, process); context.Processes.TryAdd(process.Pid, process);

View File

@ -277,7 +277,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
return result; return result;
} }
result = Capabilities.InitializeForUser(capabilities, MemoryManager); result = Capabilities.InitializeForUser(capabilities, MemoryManager, IsApplication);
if (result != Result.Success) if (result != Result.Success)
{ {

View File

@ -35,15 +35,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
DebuggingFlags &= ~3u; DebuggingFlags &= ~3u;
KernelReleaseVersion = KProcess.KernelVersionPacked; 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 mask0 = 0;
int mask1 = 0; int mask1 = 0;
@ -54,7 +54,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
if (cap.GetCapabilityType() != CapabilityType.MapRange) 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) if (result != Result.Success)
{ {
@ -120,7 +120,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
return Result.Success; 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(); CapabilityType code = cap.GetCapabilityType();
@ -176,6 +176,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
AllowedCpuCoresMask = GetMaskFromMinMax(lowestCpuCore, highestCpuCore); AllowedCpuCoresMask = GetMaskFromMinMax(lowestCpuCore, highestCpuCore);
AllowedThreadPriosMask = GetMaskFromMinMax(lowestThreadPrio, highestThreadPrio); 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; break;
} }

View File

@ -2683,7 +2683,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall
return KernelResult.InvalidCombination; return KernelResult.InvalidCombination;
} }
if ((uint)preferredCore > 3) if ((uint)preferredCore > KScheduler.CpuCoresCount - 1)
{ {
if ((preferredCore | 2) != -1) if ((preferredCore | 2) != -1)
{ {

View File

@ -9,13 +9,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
partial class KScheduler : IDisposable partial class KScheduler : IDisposable
{ {
public const int PrioritiesCount = 64; public const int PrioritiesCount = 64;
public const int CpuCoresCount = 4; public static int CpuCoresCount;
private const int RoundRobinTimeQuantumMs = 10; private const int RoundRobinTimeQuantumMs = 10;
private static readonly int[] _preemptionPriorities = { 59, 59, 59, 63 }; private static int[] _srcCoresHighestPrioThreads;
private static readonly int[] _srcCoresHighestPrioThreads = new int[CpuCoresCount];
private readonly KernelContext _context; private readonly KernelContext _context;
private readonly int _coreId; private readonly int _coreId;
@ -47,6 +45,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
_coreId = coreId; _coreId = coreId;
_currentThread = null; _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) public static ulong SelectThreads(KernelContext context)
@ -437,7 +445,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
for (int core = 0; core < CpuCoresCount; core++) for (int core = 0; core < CpuCoresCount; core++)
{ {
RotateScheduledQueue(context, core, _preemptionPriorities[core]); RotateScheduledQueue(context, core, PreemptionPriorities(core));
} }
context.CriticalSection.Leave(); context.CriticalSection.Leave();

View File

@ -24,14 +24,14 @@ namespace Ryujinx.HLE.HOS.Services
// not large enough. // not large enough.
private const int PointerBufferSize = 0x8000; private const int PointerBufferSize = 0x8000;
private readonly static uint[] _defaultCapabilities = { private static uint[] _defaultCapabilities => [
0x030363F7, (((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u,
0x1FFFFFCF, 0x1FFFFFCF,
0x207FFFEF, 0x207FFFEF,
0x47E0060F, 0x47E0060F,
0x0048BFFF, 0x0048BFFF,
0x01007FFF, 0x01007FFF,
}; ];
// The amount of time Dispose() will wait to Join() the thread executing the ServerLoop() // The amount of time Dispose() will wait to Join() the thread executing the ServerLoop()
private static readonly TimeSpan _threadJoinTimeout = TimeSpan.FromSeconds(3); private static readonly TimeSpan _threadJoinTimeout = TimeSpan.FromSeconds(3);

View File

@ -32,6 +32,8 @@ namespace Ryujinx.HLE
public TamperMachine TamperMachine { get; } public TamperMachine TamperMachine { get; }
public IHostUIHandler UIHandler { get; } public IHostUIHandler UIHandler { get; }
public int CpuCoresCount = 4; //Switch 1 has 4 cores
public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch; public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch;
public bool CustomVSyncIntervalEnabled { get; set; } = false; public bool CustomVSyncIntervalEnabled { get; set; } = false;
public int CustomVSyncInterval { get; set; } public int CustomVSyncInterval { get; set; }

View File

@ -402,7 +402,7 @@
<x:Double x:Key="ControlContentThemeFontSize">13</x:Double> <x:Double x:Key="ControlContentThemeFontSize">13</x:Double>
<x:Double x:Key="MenuItemHeight">26</x:Double> <x:Double x:Key="MenuItemHeight">26</x:Double>
<x:Double x:Key="TabItemMinHeight">28</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> <x:Double x:Key="ContentDialogMaxHeight">756</x:Double>
</Styles.Resources> </Styles.Resources>
</Styles> </Styles>

View File

@ -22597,6 +22597,56 @@
"zh_TW": "" "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", "ID": "CompatibilityListSearchBoxWatermark",
"Translations": { "Translations": {

View File

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

View File

@ -145,6 +145,9 @@
<EmbeddedResource Include="..\..\distribution\macos\shortcut-template.plist"> <EmbeddedResource Include="..\..\distribution\macos\shortcut-template.plist">
<Link>Assets\ShortcutFiles\shortcut-template.plist</Link> <Link>Assets\ShortcutFiles\shortcut-template.plist</Link>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Include="..\..\docs\compatibility.csv" LogicalName="RyujinxGameCompatibilityList">
<Link>Assets\RyujinxGameCompatibility.csv</Link>
</EmbeddedResource>
<EmbeddedResource Include="Assets\locales.json" /> <EmbeddedResource Include="Assets\locales.json" />
<EmbeddedResource Include="Assets\Styles\Styles.xaml" /> <EmbeddedResource Include="Assets\Styles\Styles.xaml" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" /> <EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
@ -168,12 +171,6 @@
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="Assets\locales.json" /> <AdditionalFiles Include="Assets\locales.json" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="Utilities\Compat\CompatibilityList.axaml.cs">
<DependentUpon>CompatibilityList.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Assets\Fonts\Mono\" /> <Folder Include="Assets\Fonts\Mono\" />
</ItemGroup> </ItemGroup>

View File

@ -133,12 +133,13 @@
Spacing="5"> Spacing="5">
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{Binding TimePlayedString}" Text="{Binding LastPlayedString}"
TextAlignment="End" TextAlignment="End"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Text="{Binding LastPlayedString}" Text="{Binding TimePlayedString}"
IsVisible="{Binding HasPlayedPreviously}"
TextAlignment="End" TextAlignment="End"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock <TextBlock

View File

@ -12,8 +12,8 @@ namespace Ryujinx.Ava.UI.Helpers
private static readonly Lazy<PlayabilityStatusConverter> _shared = new(() => new()); private static readonly Lazy<PlayabilityStatusConverter> _shared = new(() => new());
public static PlayabilityStatusConverter Shared => _shared.Value; public static PlayabilityStatusConverter Shared => _shared.Value;
public object Convert(object? value, Type _, object? __, CultureInfo ___) => public object Convert(object value, Type _, object __, CultureInfo ___)
value.Cast<LocaleKeys>() switch => value.Cast<LocaleKeys>() switch
{ {
LocaleKeys.CompatibilityListNothing or LocaleKeys.CompatibilityListNothing or
LocaleKeys.CompatibilityListBoots or LocaleKeys.CompatibilityListBoots or
@ -22,7 +22,7 @@ namespace Ryujinx.Ava.UI.Helpers
_ => Brushes.ForestGreen _ => Brushes.ForestGreen
}; };
public object ConvertBack(object? value, Type _, object? __, CultureInfo ___) public object ConvertBack(object value, Type _, object __, CultureInfo ___)
=> throw new NotSupportedException(); => 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(Action callback) => new MiniCommand<object>(_ => callback());
public static MiniCommand Create<TArg>(Action<TArg> callback) => new MiniCommand<TArg>(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(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 bool CanExecute(object parameter);
public abstract void Execute(object parameter); public abstract void Execute(object parameter);

View File

@ -1,152 +1,53 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.Ava.UI.Models.Input namespace Ryujinx.Ava.UI.Models.Input
{ {
public class HotkeyConfig : BaseModel public partial class HotkeyConfig : BaseModel
{ {
private Key _toggleVSyncMode; [ObservableProperty] private Key _toggleVSyncMode;
public Key ToggleVSyncMode
{
get => _toggleVSyncMode;
set
{
_toggleVSyncMode = value;
OnPropertyChanged();
}
}
private Key _screenshot; [ObservableProperty] private Key _screenshot;
public Key Screenshot
{
get => _screenshot;
set
{
_screenshot = value;
OnPropertyChanged();
}
}
private Key _showUI; [ObservableProperty] private Key _showUI;
public Key ShowUI
{
get => _showUI;
set
{
_showUI = value;
OnPropertyChanged();
}
}
private Key _pause; [ObservableProperty] private Key _pause;
public Key Pause
{
get => _pause;
set
{
_pause = value;
OnPropertyChanged();
}
}
private Key _toggleMute; [ObservableProperty] private Key _toggleMute;
public Key ToggleMute
{
get => _toggleMute;
set
{
_toggleMute = value;
OnPropertyChanged();
}
}
private Key _resScaleUp; [ObservableProperty] private Key _resScaleUp;
public Key ResScaleUp
{
get => _resScaleUp;
set
{
_resScaleUp = value;
OnPropertyChanged();
}
}
private Key _resScaleDown; [ObservableProperty] private Key _resScaleDown;
public Key ResScaleDown
{
get => _resScaleDown;
set
{
_resScaleDown = value;
OnPropertyChanged();
}
}
private Key _volumeUp; [ObservableProperty] private Key _volumeUp;
public Key VolumeUp
{
get => _volumeUp;
set
{
_volumeUp = value;
OnPropertyChanged();
}
}
private Key _volumeDown; [ObservableProperty] private Key _volumeDown;
public Key VolumeDown
{
get => _volumeDown;
set
{
_volumeDown = value;
OnPropertyChanged();
}
}
private Key _customVSyncIntervalIncrement; [ObservableProperty] private Key _customVSyncIntervalIncrement;
public Key CustomVSyncIntervalIncrement
{
get => _customVSyncIntervalIncrement;
set
{
_customVSyncIntervalIncrement = value;
OnPropertyChanged();
}
}
private Key _customVSyncIntervalDecrement; [ObservableProperty] private Key _customVSyncIntervalDecrement;
public Key CustomVSyncIntervalDecrement
{
get => _customVSyncIntervalDecrement;
set
{
_customVSyncIntervalDecrement = value;
OnPropertyChanged();
}
}
public HotkeyConfig(KeyboardHotkeys config) public HotkeyConfig(KeyboardHotkeys config)
{ {
if (config != null) if (config == null)
{ return;
ToggleVSyncMode = config.ToggleVSyncMode;
Screenshot = config.Screenshot; ToggleVSyncMode = config.ToggleVSyncMode;
ShowUI = config.ShowUI; Screenshot = config.Screenshot;
Pause = config.Pause; ShowUI = config.ShowUI;
ToggleMute = config.ToggleMute; Pause = config.Pause;
ResScaleUp = config.ResScaleUp; ToggleMute = config.ToggleMute;
ResScaleDown = config.ResScaleDown; ResScaleUp = config.ResScaleUp;
VolumeUp = config.VolumeUp; ResScaleDown = config.ResScaleDown;
VolumeDown = config.VolumeDown; VolumeUp = config.VolumeUp;
CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement; VolumeDown = config.VolumeDown;
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement; CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement;
} CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
} }
public KeyboardHotkeys GetConfig() public KeyboardHotkeys GetConfig() =>
{ new()
var config = new KeyboardHotkeys
{ {
ToggleVSyncMode = ToggleVSyncMode, ToggleVSyncMode = ToggleVSyncMode,
Screenshot = Screenshot, Screenshot = Screenshot,
@ -160,8 +61,5 @@ namespace Ryujinx.Ava.UI.Models.Input
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement, CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement, CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
}; };
return config;
}
} }
} }

View File

@ -1,21 +1,13 @@
using Avalonia.Svg.Skia; using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Views.Input; using Ryujinx.Ava.UI.Views.Input;
namespace Ryujinx.Ava.UI.ViewModels.Input namespace Ryujinx.Ava.UI.ViewModels.Input
{ {
public class ControllerInputViewModel : BaseModel public partial class ControllerInputViewModel : BaseModel
{ {
private GamepadInputConfig _config; [ObservableProperty] private GamepadInputConfig _config;
public GamepadInputConfig Config
{
get => _config;
set
{
_config = value;
OnPropertyChanged();
}
}
private bool _isLeft; private bool _isLeft;
public bool IsLeft public bool IsLeft
@ -43,16 +35,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool HasSides => IsLeft ^ IsRight; public bool HasSides => IsLeft ^ IsRight;
private SvgImage _image; [ObservableProperty] private SvgImage _image;
public SvgImage Image
{
get => _image;
set
{
_image = value;
OnPropertyChanged();
}
}
public readonly InputViewModel ParentModel; public readonly InputViewModel ParentModel;

View File

@ -1,9 +1,8 @@
using Avalonia;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Svg.Skia; using Avalonia.Svg.Skia;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input; using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
@ -32,7 +31,7 @@ using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Ava.UI.ViewModels.Input namespace Ryujinx.Ava.UI.ViewModels.Input
{ {
public class InputViewModel : BaseModel, IDisposable public partial class InputViewModel : BaseModel, IDisposable
{ {
private const string Disabled = "disabled"; private const string Disabled = "disabled";
private const string ProControllerResource = "Ryujinx/Assets/Icons/Controller_ProCon.svg"; private const string ProControllerResource = "Ryujinx/Assets/Icons/Controller_ProCon.svg";
@ -48,8 +47,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private int _controller; private int _controller;
private string _controllerImage; private string _controllerImage;
private int _device; private int _device;
private object _configViewModel; [ObservableProperty] private object _configViewModel;
private string _profileName; [ObservableProperty] private string _profileName;
private bool _isLoaded; private bool _isLoaded;
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@ -73,17 +72,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool IsModified { get; set; } public bool IsModified { get; set; }
public event Action NotifyChangesEvent; public event Action NotifyChangesEvent;
public object ConfigViewModel
{
get => _configViewModel;
set
{
_configViewModel = value;
OnPropertyChanged();
}
}
public PlayerIndex PlayerIdChoose public PlayerIndex PlayerIdChoose
{ {
get => _playerIdChoose; get => _playerIdChoose;
@ -200,16 +188,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
} }
} }
public string ProfileName
{
get => _profileName; set
{
_profileName = value;
OnPropertyChanged();
}
}
public int Device public int Device
{ {
get => _device; get => _device;

View File

@ -1,20 +1,12 @@
using Avalonia.Svg.Skia; using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.Models.Input; using Ryujinx.Ava.UI.Models.Input;
namespace Ryujinx.Ava.UI.ViewModels.Input namespace Ryujinx.Ava.UI.ViewModels.Input
{ {
public class KeyboardInputViewModel : BaseModel public partial class KeyboardInputViewModel : BaseModel
{ {
private KeyboardInputConfig _config; [ObservableProperty] private KeyboardInputConfig _config;
public KeyboardInputConfig Config
{
get => _config;
set
{
_config = value;
OnPropertyChanged();
}
}
private bool _isLeft; private bool _isLeft;
public bool IsLeft public bool IsLeft
@ -42,16 +34,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool HasSides => IsLeft ^ IsRight; public bool HasSides => IsLeft ^ IsRight;
private SvgImage _image; [ObservableProperty] private SvgImage _image;
public SvgImage Image
{
get => _image;
set
{
_image = value;
OnPropertyChanged();
}
}
public readonly InputViewModel ParentModel; public readonly InputViewModel ParentModel;

View File

@ -1,93 +1,23 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ryujinx.Ava.UI.ViewModels.Input namespace Ryujinx.Ava.UI.ViewModels.Input
{ {
public class MotionInputViewModel : BaseModel public partial class MotionInputViewModel : BaseModel
{ {
private int _slot; [ObservableProperty] private int _slot;
public int Slot
{
get => _slot;
set
{
_slot = value;
OnPropertyChanged();
}
}
private int _altSlot; [ObservableProperty] private int _altSlot;
public int AltSlot
{
get => _altSlot;
set
{
_altSlot = value;
OnPropertyChanged();
}
}
private string _dsuServerHost; [ObservableProperty] private string _dsuServerHost;
public string DsuServerHost
{
get => _dsuServerHost;
set
{
_dsuServerHost = value;
OnPropertyChanged();
}
}
private int _dsuServerPort; [ObservableProperty] private int _dsuServerPort;
public int DsuServerPort
{
get => _dsuServerPort;
set
{
_dsuServerPort = value;
OnPropertyChanged();
}
}
private bool _mirrorInput; [ObservableProperty] private bool _mirrorInput;
public bool MirrorInput
{
get => _mirrorInput;
set
{
_mirrorInput = value;
OnPropertyChanged();
}
}
private int _sensitivity; [ObservableProperty] private int _sensitivity;
public int Sensitivity
{
get => _sensitivity;
set
{
_sensitivity = value;
OnPropertyChanged();
}
}
private double _gryoDeadzone; [ObservableProperty] private double _gyroDeadzone;
public double GyroDeadzone
{
get => _gryoDeadzone;
set
{
_gryoDeadzone = value;
OnPropertyChanged();
}
}
private bool _enableCemuHookMotion; [ObservableProperty] private bool _enableCemuHookMotion;
public bool EnableCemuHookMotion
{
get => _enableCemuHookMotion;
set
{
_enableCemuHookMotion = value;
OnPropertyChanged();
}
}
} }
} }

View File

@ -1,27 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ryujinx.Ava.UI.ViewModels.Input namespace Ryujinx.Ava.UI.ViewModels.Input
{ {
public class RumbleInputViewModel : BaseModel public partial class RumbleInputViewModel : BaseModel
{ {
private float _strongRumble; [ObservableProperty] private float _strongRumble;
public float StrongRumble
{
get => _strongRumble;
set
{
_strongRumble = value;
OnPropertyChanged();
}
}
private float _weakRumble; [ObservableProperty] private float _weakRumble;
public float WeakRumble
{
get => _weakRumble;
set
{
_weakRumble = value;
OnPropertyChanged();
}
}
} }
} }

View File

@ -741,7 +741,10 @@ namespace Ryujinx.Ava.UI.ViewModels
Applications.ToObservableChangeSet() Applications.ToObservableChangeSet()
.Filter(Filter) .Filter(Filter)
.Sort(GetComparer()) .Sort(GetComparer())
.Bind(out _appsObservableList).AsObservableList(); #pragma warning disable MVVMTK0034
.Bind(out _appsObservableList)
#pragma warning restore MVVMTK0034
.AsObservableList();
OnPropertyChanged(nameof(AppsObservableList)); OnPropertyChanged(nameof(AppsObservableList));
} }

View File

@ -56,7 +56,7 @@
ToolTip.Tip="{ext:Locale LoadTitleUpdatesFromFolderTooltip}" /> ToolTip.Tip="{ext:Locale LoadTitleUpdatesFromFolderTooltip}" />
<MenuItem Header="{ext:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}" Icon="{ext:Icon mdi-launch}"> <MenuItem Header="{ext:Locale MenuBarFileOpenApplet}" IsEnabled="{Binding IsAppletMenuActive}" Icon="{ext:Icon mdi-launch}">
<MenuItem <MenuItem
Click="OpenMiiApplet" Name="MiiAppletMenuItem"
Header="{ext:Locale MenuBarFileOpenAppletOpenMiiApplet}" Header="{ext:Locale MenuBarFileOpenAppletOpenMiiApplet}"
Icon="{ext:Icon fa-solid fa-person}" Icon="{ext:Icon fa-solid fa-person}"
ToolTip.Tip="{ext:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" /> ToolTip.Tip="{ext:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" />
@ -72,7 +72,7 @@
ToolTip.Tip="{ext:Locale OpenRyujinxLogsTooltip}" /> ToolTip.Tip="{ext:Locale OpenRyujinxLogsTooltip}" />
<Separator /> <Separator />
<MenuItem <MenuItem
Click="CloseWindow" Name="CloseRyujinxMenuItem"
Header="{ext:Locale MenuBarFileExit}" Header="{ext:Locale MenuBarFileExit}"
Icon="{ext:Icon fa-solid fa-xmark}" Icon="{ext:Icon fa-solid fa-xmark}"
ToolTip.Tip="{ext:Locale ExitTooltip}" /> ToolTip.Tip="{ext:Locale ExitTooltip}" />
@ -167,7 +167,7 @@
Header="{ext:Locale MenuBarShowFileTypes}" /> Header="{ext:Locale MenuBarShowFileTypes}" />
<Separator /> <Separator />
<MenuItem <MenuItem
Click="OpenSettings" Name="OpenSettingsMenuItem"
Padding="0" Padding="0"
Header="{ext:Locale MenuBarOptionsSettings}" Header="{ext:Locale MenuBarOptionsSettings}"
Icon="{ext:Icon fa-solid fa-gear}" Icon="{ext:Icon fa-solid fa-gear}"
@ -210,21 +210,21 @@
Header="{ext:Locale MenuBarActions}" Header="{ext:Locale MenuBarActions}"
IsEnabled="{Binding IsGameRunning}"> IsEnabled="{Binding IsGameRunning}">
<MenuItem <MenuItem
Click="PauseEmulation_Click" Name="PauseEmulationMenuItem"
Header="{ext:Locale MenuBarOptionsPauseEmulation}" Header="{ext:Locale MenuBarOptionsPauseEmulation}"
Icon="{ext:Icon fa-solid fa-pause}" Icon="{ext:Icon fa-solid fa-pause}"
InputGesture="{Binding PauseKey}" InputGesture="{Binding PauseKey}"
IsEnabled="{Binding !IsPaused}" IsEnabled="{Binding !IsPaused}"
IsVisible="{Binding !IsPaused}" /> IsVisible="{Binding !IsPaused}" />
<MenuItem <MenuItem
Click="ResumeEmulation_Click" Name="ResumeEmulationMenuItem"
Header="{ext:Locale MenuBarOptionsResumeEmulation}" Header="{ext:Locale MenuBarOptionsResumeEmulation}"
Icon="{ext:Icon fa-solid fa-play}" Icon="{ext:Icon fa-solid fa-play}"
InputGesture="{Binding PauseKey}" InputGesture="{Binding PauseKey}"
IsEnabled="{Binding IsPaused}" IsEnabled="{Binding IsPaused}"
IsVisible="{Binding IsPaused}" /> IsVisible="{Binding IsPaused}" />
<MenuItem <MenuItem
Click="StopEmulation_Click" Name="StopEmulationMenuItem"
Header="{ext:Locale MenuBarOptionsStopEmulation}" Header="{ext:Locale MenuBarOptionsStopEmulation}"
Icon="{ext:Icon fa-solid fa-stop}" Icon="{ext:Icon fa-solid fa-stop}"
InputGesture="Escape" InputGesture="Escape"
@ -233,17 +233,15 @@
<MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{ext:Locale MenuBarOptionsSimulateWakeUpMessage}" /> <MenuItem Command="{Binding SimulateWakeUpMessage}" Header="{ext:Locale MenuBarOptionsSimulateWakeUpMessage}" />
<Separator /> <Separator />
<MenuItem <MenuItem
Name="ScanAmiiboMenuItem" Command="{Binding OpenAmiiboWindow}"
AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree" AttachedToVisualTree="ScanAmiiboMenuItem_AttachedToVisualTree"
Click="OpenAmiiboWindow"
Header="{ext:Locale MenuBarActionsScanAmiibo}" Header="{ext:Locale MenuBarActionsScanAmiibo}"
Icon="{ext:Icon mdi-cube-scan}" Icon="{ext:Icon mdi-cube-scan}"
InputGesture="Ctrl + A" InputGesture="Ctrl + A"
IsEnabled="{Binding IsAmiiboRequested}" /> IsEnabled="{Binding IsAmiiboRequested}" />
<MenuItem <MenuItem
Name="ScanAmiiboMenuItemFromBin" Command="{Binding OpenBinFile}"
AttachedToVisualTree="ScanBinAmiiboMenuItem_AttachedToVisualTree" AttachedToVisualTree="ScanBinAmiiboMenuItem_AttachedToVisualTree"
Click="OpenBinFile"
Header="{ext:Locale MenuBarActionsScanAmiiboBin}" Header="{ext:Locale MenuBarActionsScanAmiiboBin}"
Icon="{ext:Icon mdi-cube-scan}" Icon="{ext:Icon mdi-cube-scan}"
IsVisible="{Binding CanScanAmiiboBinaries}" IsVisible="{Binding CanScanAmiiboBinaries}"
@ -262,7 +260,7 @@
InputGesture="{Binding ShowUiKey}" InputGesture="{Binding ShowUiKey}"
IsEnabled="{Binding IsGameRunning}" /> IsEnabled="{Binding IsGameRunning}" />
<MenuItem <MenuItem
Click="OpenCheatManagerForCurrentApp" Name="CheatManagerMenuItem"
Header="{ext:Locale GameListContextMenuManageCheat}" Header="{ext:Locale GameListContextMenuManageCheat}"
Icon="{ext:Icon fa-solid fa-code}" Icon="{ext:Icon fa-solid fa-code}"
IsEnabled="{Binding IsGameRunning}" /> IsEnabled="{Binding IsGameRunning}" />
@ -277,56 +275,55 @@
<MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" Icon="{ext:Icon mdi-folder-cog}" /> <MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" Icon="{ext:Icon mdi-folder-cog}" />
</MenuItem> </MenuItem>
<MenuItem Header="{ext:Locale MenuBarToolsManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}"> <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 Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBarToolsInstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" />
<MenuItem Header="{ext:Locale MenuBarToolsUninstallFileTypes}" Click="UninstallFileTypes_Click" IsEnabled="{Binding AreMimeTypesRegistered}" /> <MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBarToolsUninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" />
</MenuItem> </MenuItem>
<Separator /> <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>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarView}"> <MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarView}">
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarViewWindow}"> <MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarViewWindow}">
<MenuItem Header="{ext:Locale MenuBarViewWindow720}" Tag="1280 720" Click="ChangeWindowSize_Click" /> <MenuItem Name="WindowSize720PMenuItem" Header="{ext:Locale MenuBarViewWindow720}" CommandParameter="1280 720" />
<MenuItem Header="{ext:Locale MenuBarViewWindow1080}" Tag="1920 1080" Click="ChangeWindowSize_Click" /> <MenuItem Name="WindowSize1080PMenuItem" Header="{ext:Locale MenuBarViewWindow1080}" CommandParameter="1920 1080" />
<MenuItem Header="{ext:Locale MenuBarViewWindow1440}" Tag="2560 1440" Click="ChangeWindowSize_Click" /> <MenuItem Name="WindowSize1440PMenuItem" Header="{ext:Locale MenuBarViewWindow1440}" CommandParameter="2560 1440" />
<MenuItem Header="{ext:Locale MenuBarViewWindow2160}" Tag="3840 2160" Click="ChangeWindowSize_Click" /> <MenuItem Name="WindowSize2160PMenuItem" Header="{ext:Locale MenuBarViewWindow2160}" CommandParameter="3840 2160" />
</MenuItem> </MenuItem>
</MenuItem> </MenuItem>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelp}"> <MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelp}">
<MenuItem <MenuItem
Click="OpenAboutWindow" Name="AboutWindowMenuItem"
Header="{ext:Locale MenuBarHelpAbout}" Header="{ext:Locale MenuBarHelpAbout}"
Icon="{ext:Icon fa-solid fa-circle-info}" Icon="{ext:Icon fa-solid fa-circle-info}"
ToolTip.Tip="{ext:Locale OpenAboutTooltip}" /> ToolTip.Tip="{ext:Locale OpenAboutTooltip}" />
<MenuItem <MenuItem
Name="UpdateMenuItem" Name="UpdateMenuItem"
IsEnabled="{Binding CanUpdate}" IsEnabled="{Binding CanUpdate}"
Click="CheckForUpdates"
Header="{ext:Locale MenuBarHelpCheckForUpdates}" Header="{ext:Locale MenuBarHelpCheckForUpdates}"
Icon="{ext:Icon mdi-update}" Icon="{ext:Icon mdi-update}"
ToolTip.Tip="{ext:Locale CheckUpdatesTooltip}" /> ToolTip.Tip="{ext:Locale CheckUpdatesTooltip}" />
<MenuItem <MenuItem
Click="OpenCompatibilityList" Name="CompatibilityListMenuItem"
Header="{ext:Locale CompatibilityListOpen}" Header="{ext:Locale CompatibilityListOpen}"
Icon="{ext:Icon mdi-gamepad}"/> Icon="{ext:Icon mdi-gamepad}"/>
<Separator /> <Separator />
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelpFaqAndGuides}" Icon="{ext:Icon fa-solid fa-question}" > <MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarHelpFaqAndGuides}" Icon="{ext:Icon fa-solid fa-question}" >
<MenuItem <MenuItem
Click="MenuItem_OnClick" Name="FaqMenuItem"
Header="{ext:Locale MenuBarHelpFaq}" Header="{ext:Locale MenuBarHelpFaq}"
Icon="{ext:Icon fa-github}" 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}" /> ToolTip.Tip="{ext:Locale MenuBarHelpFaqTooltip}" />
<MenuItem <MenuItem
Click="MenuItem_OnClick" Name="SetupGuideMenuItem"
Header="{ext:Locale MenuBarHelpSetup}" Header="{ext:Locale MenuBarHelpSetup}"
Icon="{ext:Icon fa-github}" 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}" /> ToolTip.Tip="{ext:Locale MenuBarHelpSetupTooltip}" />
<MenuItem <MenuItem
Click="MenuItem_OnClick" Name="LdnGuideMenuItem"
Header="{ext:Locale MenuBarHelpMultiplayer}" Header="{ext:Locale MenuBarHelpMultiplayer}"
Icon="{ext:Icon fa-github}" 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}" /> ToolTip.Tip="{ext:Locale MenuBarHelpMultiplayerTooltip}" />
</MenuItem> </MenuItem>
</MenuItem> </MenuItem>

View File

@ -1,7 +1,8 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Layout;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.Input;
using Gommon; using Gommon;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
@ -17,6 +18,7 @@ using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Views.Main namespace Ryujinx.Ava.UI.Views.Main
{ {
@ -34,9 +36,37 @@ namespace Ryujinx.Ava.UI.Views.Main
ToggleFileTypesMenuItem.ItemsSource = GenerateToggleFileTypeItems(); ToggleFileTypesMenuItem.ItemsSource = GenerateToggleFileTypeItems();
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems(); 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>() Enum.GetValues<FileTypes>()
.Select(it => (FileName: Enum.GetName(it)!, FileType: it)) .Select(it => (FileName: Enum.GetName(it)!, FileType: it))
.Select(it => .Select(it =>
@ -46,15 +76,13 @@ namespace Ryujinx.Ava.UI.Views.Main
IsChecked = it.FileType.GetConfigValue(ConfigurationState.Instance.UI.ShownFileTypes), IsChecked = it.FileType.GetConfigValue(ConfigurationState.Instance.UI.ShownFileTypes),
Command = MiniCommand.Create(() => Window.ToggleFileType(it.FileName)) 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); LocalesJson locales = JsonHelper.Deserialize(languageJson, LocalesJsonContext.Default.LocalesJson);
@ -69,20 +97,23 @@ namespace Ryujinx.Ava.UI.Views.Main
} }
else 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() MenuItem menuItem = new()
{ {
Padding = new Thickness(10, 0, 0, 0), Padding = new Thickness(15, 0, 0, 0),
Header = " " + languageName, Margin = new Thickness(3, 0, 3, 0),
HorizontalAlignment = HorizontalAlignment.Stretch,
Header = languageName,
Command = MiniCommand.Create(() => MainWindowViewModel.ChangeLanguage(language)) Command = MiniCommand.Create(() => MainWindowViewModel.ChangeLanguage(language))
}; };
menuItems.Add(menuItem); yield return menuItem;
} }
return menuItems.ToArray();
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) 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) public async Task OpenSettings()
{
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)
{ {
Window.SettingsWindow = new(Window.VirtualFileSystem, Window.ContentManager); 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 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)) 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) public async Task OpenCheatManagerForCurrentApp()
=> await ViewModel.OpenAmiiboWindow();
public async void OpenBinFile(object sender, RoutedEventArgs e)
=> await ViewModel.OpenBinFile();
public async void OpenCheatManagerForCurrentApp(object sender, RoutedEventArgs e)
{ {
if (!ViewModel.IsGameRunning) if (!ViewModel.IsGameRunning)
return; return;
@ -166,7 +176,7 @@ namespace Ryujinx.Ava.UI.Views.Main
ViewModel.IsAmiiboBinRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasAmiiboKeyFile; ViewModel.IsAmiiboBinRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasAmiiboKeyFile;
} }
private async void InstallFileTypes_Click(object sender, RoutedEventArgs e) private async Task InstallFileTypes()
{ {
ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install(); ViewModel.AreMimeTypesRegistered = FileAssociationHelper.Install();
if (ViewModel.AreMimeTypesRegistered) if (ViewModel.AreMimeTypesRegistered)
@ -175,7 +185,7 @@ namespace Ryujinx.Ava.UI.Views.Main
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]); await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogInstallFileTypesErrorMessage]);
} }
private async void UninstallFileTypes_Click(object sender, RoutedEventArgs e) private async Task UninstallFileTypes()
{ {
ViewModel.AreMimeTypesRegistered = !FileAssociationHelper.Uninstall(); ViewModel.AreMimeTypesRegistered = !FileAssociationHelper.Uninstall();
if (!ViewModel.AreMimeTypesRegistered) if (!ViewModel.AreMimeTypesRegistered)
@ -184,11 +194,8 @@ namespace Ryujinx.Ava.UI.Views.Main
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogUninstallFileTypesErrorMessage]); 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) (int resolutionWidth, int resolutionHeight) = resolution.Split(' ', 2)
.Into(parts => .Into(parts =>
(int.Parse(parts[0]), int.Parse(parts[1])) (int.Parse(parts[0]), int.Parse(parts[1]))
@ -201,7 +208,7 @@ namespace Ryujinx.Ava.UI.Views.Main
double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor); double windowWidthScaled = (resolutionWidth * Program.WindowScaleFactor);
double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor); double windowHeightScaled = ((resolutionHeight + barsHeight) * Program.WindowScaleFactor);
await Dispatcher.UIThread.InvokeAsync(() => Dispatcher.UIThread.Post(() =>
{ {
ViewModel.WindowState = WindowState.Normal; ViewModel.WindowState = WindowState.Normal;
@ -209,24 +216,7 @@ namespace Ryujinx.Ava.UI.Views.Main
}); });
} }
public async void CheckForUpdates(object sender, RoutedEventArgs e) public void CloseWindow() => Window.Close();
{
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 CompatibilityList.Show();
} }
} }

View File

@ -164,7 +164,7 @@ namespace Ryujinx.Ava.UI.Windows
}); });
} }
private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e) private void ApplicationLibrary_LdnGameDataReceived(LdnGameDataReceivedEventArgs e)
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
@ -408,13 +408,10 @@ namespace Ryujinx.Ava.UI.Windows
{ {
StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged; StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged;
ApplicationGrid.DataContext = ApplicationList.DataContext = ViewModel;
ApplicationGrid.ApplicationOpened += Application_Opened; ApplicationGrid.ApplicationOpened += Application_Opened;
ApplicationGrid.DataContext = ViewModel;
ApplicationList.ApplicationOpened += Application_Opened; ApplicationList.ApplicationOpened += Application_Opened;
ApplicationList.DataContext = ViewModel;
} }
private void SetWindowSizePosition() private void SetWindowSizePosition()

View File

@ -1,4 +1,6 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using FluentAvalonia.Core; using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
@ -23,6 +25,11 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent(); InitializeComponent();
Load(); Load();
#if DEBUG
this.AttachDevTools(new KeyGesture(Key.F12, KeyModifiers.Alt));
#endif
} }
public SettingsWindow() public SettingsWindow()

View File

@ -37,6 +37,8 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed); public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
public bool HasPlayedPreviously => TimePlayedString != string.Empty;
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n"); public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);

View File

@ -45,7 +45,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public const string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com"; public const string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
public Language DesiredLanguage { get; set; } public Language DesiredLanguage { get; set; }
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated; public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
public event EventHandler<LdnGameDataReceivedEventArgs> LdnGameDataReceived; public event Action<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
public readonly IObservableCache<ApplicationData, ulong> Applications; public readonly IObservableCache<ApplicationData, ulong> Applications;
public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates; public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates;
@ -779,7 +779,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
using HttpClient httpClient = new HttpClient(); using HttpClient httpClient = new HttpClient();
string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games"); string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games");
ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData); ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData);
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{ {
LdnData = ldnGameDataArray LdnData = ldnGameDataArray
}); });
@ -787,7 +787,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
catch (Exception ex) catch (Exception ex)
{ {
Logger.Warning?.Print(LogClass.Application, $"Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.\n{ex.Message}"); Logger.Warning?.Print(LogClass.Application, $"Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.\n{ex.Message}");
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{ {
LdnData = Array.Empty<LdnGameData>() LdnData = Array.Empty<LdnGameData>()
}); });
@ -795,7 +795,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
} }
else else
{ {
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{ {
LdnData = Array.Empty<LdnGameData>() LdnData = Array.Empty<LdnGameData>()
}); });

View File

@ -32,29 +32,27 @@ namespace Ryujinx.Ava.Utilities
public string GetContentPath(ContentManager contentManager) public string GetContentPath(ContentManager contentManager)
=> (contentManager ?? _contentManager) => (contentManager ?? _contentManager)
.GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program); ?.GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program);
public bool CanStart(ContentManager contentManager, out ApplicationData appData, public bool CanStart(ContentManager contentManager, out ApplicationData appData,
out BlitStruct<ApplicationControlProperty> appControl) out BlitStruct<ApplicationControlProperty> appControl)
{ {
contentManager ??= _contentManager; contentManager ??= _contentManager;
if (contentManager == null) if (contentManager == null)
{ goto BadData;
appData = null;
appControl = new BlitStruct<ApplicationControlProperty>(0); string contentPath = GetContentPath(contentManager);
return false; if (string.IsNullOrEmpty(contentPath))
} goto BadData;
appData = new() { Name = Name, Id = ProgramId, Path = GetContentPath(contentManager) }; 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); appControl = StructHelpers.CreateCustomNacpData(Name, Version);
return true; return true;
BadData:
appData = null;
appControl = new BlitStruct<ApplicationControlProperty>(0);
return false;
} }
} }
} }

View File

@ -1,51 +1,66 @@
using Gommon; using Gommon;
using Humanizer;
using nietras.SeparatedValues; using nietras.SeparatedValues;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Logging;
using System; using System;
using System.Collections.Generic; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
namespace Ryujinx.Ava.Utilities.Compat 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 class CompatibilityCsv
{ {
public static CompatibilityCsv Shared { get; set; } static CompatibilityCsv()
public CompatibilityCsv(SepReader reader)
{ {
var entries = new List<CompatibilityEntry>(); using Stream csvStream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("RyujinxGameCompatibilityList")!;
csvStream.Position = 0;
foreach (var row in reader) using SepReader reader = Sep.Reader().From(csvStream);
{ ColumnIndices columnIndices = new(reader.Header.IndexOf);
entries.Add(new CompatibilityEntry(reader.Header, row));
}
Entries = entries.Where(x => x.Status != null) Entries = reader
.OrderBy(it => it.GameName).ToArray(); .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 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>(); string titleIdRow = ColStr(row[indices.TitleId]);
TitleId = !string.IsNullOrEmpty(titleIdRow)
? titleIdRow
: default(Optional<string>);
GameName = ColStr(row[indices.GameName]);
var titleIdRow = row[header.IndexOf("extracted_game_id")].ToString(); Labels = ColStr(row[indices.Labels]).Split(';');
if (!string.IsNullOrEmpty(titleIdRow)) Status = ColStr(row[indices.Status]).ToLower() switch
TitleId = titleIdRow;
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
{ {
"playable" => LocaleKeys.CompatibilityListPlayable, "playable" => LocaleKeys.CompatibilityListPlayable,
"ingame" => LocaleKeys.CompatibilityListIngame, "ingame" => LocaleKeys.CompatibilityListIngame,
@ -55,39 +70,41 @@ namespace Ryujinx.Ava.Utilities.Compat
_ => null _ => null
}; };
if (row[header.IndexOf("last_event_date")].TryParse<DateTime>(out var dt)) if (DateTime.TryParse(ColStr(row[indices.LastUpdated]), out var dt))
LastEvent = dt; LastUpdated = dt;
if (row[header.IndexOf("events_count")].TryParse<int>(out var eventsCount)) return;
EventCount = eventsCount;
string ColStr(SepReader.Col col) => col.ToString().Trim('"');
} }
public int IssueNumber { get; }
public string GameName { get; } public string GameName { get; }
public Optional<string> TitleId { get; } public Optional<string> TitleId { get; }
public string[] IssueLabels { get; } public string[] Labels { get; }
public LocaleKeys? Status { get; } public LocaleKeys? Status { get; }
public DateTime LastEvent { get; } public DateTime LastUpdated { get; }
public int EventCount { get; }
public string LocalizedLastUpdated =>
LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
public string LocalizedStatus => LocaleManager.Instance[Status!.Value]; 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) .Select(FormatLabelName)
.JoinToString(", "); .JoinToString(", ");
public override string ToString() public override string ToString()
{ {
var sb = new StringBuilder("CompatibilityEntry: {"); StringBuilder sb = new("CompatibilityEntry: {");
sb.Append($"{nameof(IssueNumber)}={IssueNumber}, ");
sb.Append($"{nameof(GameName)}=\"{GameName}\", "); sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
sb.Append($"{nameof(TitleId)}={TitleId}, "); 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(Status)}=\"{Status}\", ");
sb.Append($"{nameof(LastEvent)}=\"{LastEvent}\", "); sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
sb.Append($"{nameof(EventCount)}={EventCount}");
sb.Append('}'); sb.Append('}');
return sb.ToString(); return sb.ToString();
@ -143,8 +160,8 @@ namespace Ryujinx.Ava.Utilities.Compat
if (value == string.Empty) if (value == string.Empty)
return string.Empty; return string.Empty;
var firstChar = value[0]; char firstChar = value[0];
var rest = value[1..]; string rest = value[1..];
return $"{char.ToUpper(firstChar)}{rest}"; 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:local="using:Ryujinx.Ava.Utilities.Compat"
xmlns:helpers="using:Ryujinx.Ava.UI.Helpers" xmlns:helpers="using:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup" xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.Utilities.Compat.CompatibilityList" x:Class="Ryujinx.Ava.Utilities.Compat.CompatibilityList"
@ -11,25 +12,46 @@
<UserControl.DataContext> <UserControl.DataContext>
<local:CompatibilityViewModel /> <local:CompatibilityViewModel />
</UserControl.DataContext> </UserControl.DataContext>
<StackPanel Orientation="Vertical"> <Grid RowDefinitions="*,Auto,*">
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal"> <Grid
<TextBox Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" /> Grid.Row="0"
<CheckBox Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" /> HorizontalAlignment="Center"
<TextBlock Margin="-3, 0, 0, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" /> ColumnDefinitions="Auto,*"
</StackPanel> Margin="0 0 0 10">
<ScrollViewer> <ui:FontIcon
<ListBox Margin="5" Grid.Column="0"
Padding="10" 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="2">
<ListBox Margin="0,5, 0, 0"
Background="Transparent" Background="Transparent"
ItemsSource="{Binding CurrentEntries}"> ItemsSource="{Binding CurrentEntries}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:CompatibilityEntry}"> <DataTemplate DataType="{x:Type local:CompatibilityEntry}">
<Grid HorizontalAlignment="Center" Width="500" ColumnDefinitions="Auto,Auto,Auto,*" <Grid Width="750"
Margin="5"> Margin="5"
ColumnDefinitions="Auto,Auto,Auto,*"
Background="Transparent"
ToolTip.Tip="{Binding LocalizedLastUpdated}">
<TextBlock Grid.Column="0" <TextBlock Grid.Column="0"
FontFamily="{StaticResource JetBrainsMono}"
Text="{Binding GameName}" Text="{Binding GameName}"
Width="140" Width="320"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Width="135" Width="135"
@ -40,14 +62,12 @@
<TextBlock Grid.Column="2" <TextBlock Grid.Column="2"
Padding="7, 0" Padding="7, 0"
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="{StaticResource JetBrainsMono}"
Text="{Binding LocalizedStatus}" Text="{Binding LocalizedStatus}"
Width="85" Width="85"
Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}" Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextWrapping="NoWrap" /> TextWrapping="NoWrap" />
<TextBlock Grid.Column="3" <TextBlock Grid.Column="3"
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="{StaticResource JetBrainsMono}"
Text="{Binding FormattedIssueLabels}" Text="{Binding FormattedIssueLabels}"
TextWrapping="WrapWithOverflow" /> TextWrapping="WrapWithOverflow" />
</Grid> </Grid>
@ -55,5 +75,5 @@
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</ScrollViewer> </ScrollViewer>
</StackPanel> </Grid>
</UserControl> </UserControl>

View File

@ -1,11 +1,11 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Styling; using Avalonia.Styling;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using nietras.SeparatedValues;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows; using System.IO;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Ryujinx.Ava.Utilities.Compat namespace Ryujinx.Ava.Utilities.Compat
@ -14,19 +14,20 @@ namespace Ryujinx.Ava.Utilities.Compat
{ {
public static async Task Show() public static async Task Show()
{ {
await CompatibilityHelper.InitAsync();
ContentDialog contentDialog = new() ContentDialog contentDialog = new()
{ {
PrimaryButtonText = string.Empty, PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty, SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose], CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
Content = new CompatibilityList { DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary) } Content = new CompatibilityList
{
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary)
}
}; };
Style closeButton = new(x => x.Name("CloseButton")); Style closeButton = new(x => x.Name("CloseButton"));
closeButton.Setters.Add(new Setter(WidthProperty, 80d)); closeButton.Setters.Add(new Setter(WidthProperty, 80d));
Style closeButtonParent = new(x => x.Name("CommandSpace")); Style closeButtonParent = new(x => x.Name("CommandSpace"));
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, Avalonia.Layout.HorizontalAlignment.Right)); closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty, Avalonia.Layout.HorizontalAlignment.Right));
@ -41,7 +42,7 @@ namespace Ryujinx.Ava.Utilities.Compat
InitializeComponent(); InitializeComponent();
} }
private void TextBox_OnTextChanged(object? sender, TextChangedEventArgs e) private void TextBox_OnTextChanged(object sender, TextChangedEventArgs e)
{ {
if (DataContext is not CompatibilityViewModel cvm) if (DataContext is not CompatibilityViewModel cvm)
return; return;
@ -53,4 +54,3 @@ namespace Ryujinx.Ava.Utilities.Compat
} }
} }
} }

View File

@ -11,14 +11,13 @@ namespace Ryujinx.Ava.Utilities.Compat
{ {
[ObservableProperty] private bool _onlyShowOwnedGames = true; [ObservableProperty] private bool _onlyShowOwnedGames = true;
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityCsv.Shared.Entries; private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityCsv.Entries;
private readonly string[] _ownedGameTitleIds = []; private readonly string[] _ownedGameTitleIds = [];
private readonly ApplicationLibrary _appLibrary; private readonly ApplicationLibrary _appLibrary;
public IEnumerable<CompatibilityEntry> CurrentEntries => OnlyShowOwnedGames public IEnumerable<CompatibilityEntry> CurrentEntries => OnlyShowOwnedGames
? _currentEntries.Where(x => ? _currentEntries.Where(x =>
x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid)) x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid)))
|| _appLibrary.Applications.Items.Any(a => a.Name.EqualsIgnoreCase(x.GameName)))
: _currentEntries; : _currentEntries;
public CompatibilityViewModel() {} public CompatibilityViewModel() {}
@ -39,11 +38,11 @@ namespace Ryujinx.Ava.Utilities.Compat
{ {
if (string.IsNullOrEmpty(searchTerm)) if (string.IsNullOrEmpty(searchTerm))
{ {
SetEntries(CompatibilityCsv.Shared.Entries); SetEntries(CompatibilityCsv.Entries);
return; return;
} }
SetEntries(CompatibilityCsv.Shared.Entries.Where(x => SetEntries(CompatibilityCsv.Entries.Where(x =>
x.GameName.ContainsIgnoreCase(searchTerm) x.GameName.ContainsIgnoreCase(searchTerm)
|| x.TitleId.Check(tid => tid.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 Ryujinx.Ava.Common.Locale;
using System; using System;
using System.Globalization; using System.Globalization;
@ -31,7 +33,7 @@ namespace Ryujinx.Ava.Utilities
Gigabytes = 9, Gigabytes = 9,
Terabytes = 10, Terabytes = 10,
Petabytes = 11, Petabytes = 11,
Exabytes = 12, Exabytes = 12
} }
private const double SizeBase10 = 1000; private const double SizeBase10 = 1000;
@ -48,22 +50,24 @@ namespace Ryujinx.Ava.Utilities
public static string FormatTimeSpan(TimeSpan? timeSpan) public static string FormatTimeSpan(TimeSpan? timeSpan)
{ {
if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1) if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
{ return string.Empty;
// Game was never played
return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture); if (timeSpan.Value.TotalSeconds < 60)
} return timeSpan.Value.Humanize(1,
countEmptyUnits: false,
maxUnit: TimeUnit.Second,
minUnit: TimeUnit.Second);
if (timeSpan.Value.TotalDays < 1) if (timeSpan.Value.TotalMinutes < 60)
{ return timeSpan.Value.Humanize(1,
// Game was played for less than a day countEmptyUnits: false,
return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture); maxUnit: TimeUnit.Minute,
} minUnit: TimeUnit.Minute);
// Game was played for more than a day return timeSpan.Value.Humanize(1,
TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days)); countEmptyUnits: false,
string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture); maxUnit: TimeUnit.Hour,
minUnit: TimeUnit.Hour);
return $"{timeSpan.Value.Days}d, {onlyTimeString}";
} }
/// <summary> /// <summary>