Compare commits

...

13 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
39 changed files with 3593 additions and 3776 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" />

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

@ -22597,6 +22597,31 @@
"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", "ID": "CompatibilityListWarning",
"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

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

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

@ -743,7 +743,7 @@ namespace Ryujinx.Ava.UI.ViewModels
.Sort(GetComparer()) .Sort(GetComparer())
#pragma warning disable MVVMTK0034 #pragma warning disable MVVMTK0034
.Bind(out _appsObservableList) .Bind(out _appsObservableList)
#pragma warning enable MVVMTK0034 #pragma warning restore MVVMTK0034
.AsObservableList(); .AsObservableList();
OnPropertyChanged(nameof(AppsObservableList)); OnPropertyChanged(nameof(AppsObservableList));

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

@ -1,4 +1,5 @@
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 Ryujinx.Common.Logging;
@ -56,7 +57,7 @@ namespace Ryujinx.Ava.Utilities.Compat
? titleIdRow ? titleIdRow
: default(Optional<string>); : default(Optional<string>);
GameName = ColStr(row[indices.GameName]).Trim().Trim('"'); GameName = ColStr(row[indices.GameName]);
Labels = ColStr(row[indices.Labels]).Split(';'); Labels = ColStr(row[indices.Labels]).Split(';');
Status = ColStr(row[indices.Status]).ToLower() switch Status = ColStr(row[indices.Status]).ToLower() switch
@ -83,12 +84,14 @@ namespace Ryujinx.Ava.Utilities.Compat
public LocaleKeys? Status { get; } public LocaleKeys? Status { get; }
public DateTime LastUpdated { get; } public DateTime LastUpdated { 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 public string FormattedTitleId => TitleId
.OrElse(new string(' ', 16)); .OrElse(new string(' ', 16));
public string FormattedIssueLabels => Labels public string FormattedIssueLabels => Labels
.Where(it => !it.StartsWithIgnoreCase("status"))
.Select(FormatLabelName) .Select(FormatLabelName)
.JoinToString(", "); .JoinToString(", ");
@ -97,7 +100,9 @@ namespace Ryujinx.Ava.Utilities.Compat
StringBuilder sb = new("CompatibilityEntry: {"); StringBuilder sb = new("CompatibilityEntry: {");
sb.Append($"{nameof(GameName)}=\"{GameName}\", "); sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
sb.Append($"{nameof(TitleId)}={TitleId}, "); sb.Append($"{nameof(TitleId)}={TitleId}, ");
sb.Append($"{nameof(Labels)}=\"{Labels}\", "); sb.Append($"{nameof(Labels)}={
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
}, ");
sb.Append($"{nameof(Status)}=\"{Status}\", "); sb.Append($"{nameof(Status)}=\"{Status}\", ");
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\""); sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
sb.Append('}'); sb.Append('}');

View File

@ -44,8 +44,11 @@
ItemsSource="{Binding CurrentEntries}"> ItemsSource="{Binding CurrentEntries}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:CompatibilityEntry}"> <DataTemplate DataType="{x:Type local:CompatibilityEntry}">
<Grid Width="750" 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"
Text="{Binding GameName}" Text="{Binding GameName}"
Width="320" Width="320"

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>