Compare commits

..

76 Commits

Author SHA1 Message Date
Vladimir Sokolov
44de9b378b Merge branch 'master' into master 2025-01-11 22:54:24 +10:00
Vova
2ec032c48b Added description in another file 2025-01-11 22:37:33 +10:00
Vova
f75efbea54 oops, rename to"SpecialExitEmulator" 2025-01-11 22:27:28 +10:00
Vova
11f1922a82 fix specialExitEmulator -> SpecialExitEmulator, Added description to clarify function 2025-01-11 22:20:08 +10:00
Evan Husted
f9e8f4bc29 docs: compat: Ori and the Will of the Wisps is now ingame, not playable 2025-01-11 06:10:58 -06:00
Vladimir Sokolov
de18c4927f Merge branch 'master' into master 2025-01-11 21:26:51 +10:00
Evan Husted
7694c8c046 Do not auto release stable 2025-01-11 04:08:11 -06:00
Evan Husted
0dd789e8a5 misc: chore: remove redundant trimming on CompatibilityEntry.GameName init 2025-01-11 01:26:34 -06:00
Evan Husted
4e0aafd005 docs: compat: Trim redundant/duplicate information to save space 2025-01-11 01:18:10 -06:00
Evan Husted
c5091f499e docs: compat: The House of the Dead: Remake Playable 2025-01-11 00:38:32 -06:00
Evan Husted
41c8fd8194 misc: chore: lol this field was misspelled 2025-01-10 23:23:53 -06:00
Evan Husted
d4a7ee25ea misc: chore: use ObservableProperty on input view models 2025-01-10 23:23:05 -06:00
Evan Husted
3141c560fb misc: chore: remove sender parameter from LdnGameData receieved event 2025-01-10 23:15:55 -06:00
Evan Husted
de341b285b misc: use ObservableProperty on HotkeyConfig fields 2025-01-10 23:15:37 -06:00
Evan Husted
78e7a3085a add back a comment that was removed for no reason 2025-01-10 21:16:31 -06:00
Evan Husted
01e22f1c67 newline brace 2025-01-10 21:13:39 -06:00
Evan Husted
cc95e80ee9 misc: chore: Move converters into a directory in Helpers. Namespace unchanged 2025-01-10 20:24:53 -06:00
Evan Husted
d75ce52bd4 UI: Show play time in one time unit, maxing out at hours. 2025-01-10 20:23:47 -06:00
Vladimir Sokolov
3352d70ea4 Update SettingsViewModel.cs
small fix
2025-01-11 10:43:38 +10:00
Vladimir Sokolov
864cd57b51 Merge branch 'master' into master 2025-01-10 20:47:37 +10:00
Evan Husted
4a4ea557de UI: compat: show last updated date on entry hover 2025-01-10 01:43:34 -06:00
Evan Husted
33f42adb11 Merge remote-tracking branch 'origin/master' 2025-01-09 22:09:01 -06:00
LotP1
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
Evan Husted
cca429d46a misc: chore: restore not enable 2025-01-09 21:42:54 -06:00
Evan Husted
845c86f545 misc: chore: cleanup AppletMetadata.CanStart 2025-01-09 21:14:35 -06:00
Evan Husted
27993b789f misc: chore: fix some compile warnings 2025-01-09 20:23:26 -06:00
Evan Husted
bdd890cf6f UI: logger function name 2025-01-09 19:48:11 -06:00
Evan Husted
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
Evan Husted
292e27f0da UI: dispose CSV reader when done + use explicit types 2025-01-09 19:24:48 -06:00
Evan Husted
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
Vladimir Sokolov
06cd53925c Merge branch 'master' into master 2025-01-10 10:14:47 +10:00
Evan Husted
a8c3407d11 missing JP title id 2025-01-09 14:25:16 -06:00
Evan Husted
daa8168985 docs: compat: the final title IDs i could find 2025-01-09 14:03:37 -06:00
Vita Chumakova
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
Vladimir Sokolov
33fefb1323 Merge branch 'master' into master 2025-01-09 07:55:05 +10:00
Evan Husted
2226521f6c docs: compat: remove quotes around everything but game titles 2025-01-08 12:36:26 -06:00
Evan Husted
384416953d docs: compat: list title ID column first 2025-01-08 12:30:13 -06:00
Evan Husted
1343fabe41 docs: compat: some more missing title ids 2025-01-08 12:20:20 -06:00
Vladimir Sokolov
0c503e10ae Update SettingsViewModel.cs 2025-01-08 23:09:09 +10:00
Vova
1c6390cbfb minor bugs fixed 2025-01-08 23:05:58 +10:00
Vova
c20452be61 Fixed a bug where the emulator would still terminate the game when pressing a hotkey (unnecessary check removed) 2025-01-08 23:00:41 +10:00
Vova
b6667a8352 multiple fixes, variable typo fixes, adherence to a certain style. Fixed initialization of the new function, defaults to 0 2025-01-08 22:45:33 +10:00
Vova
37b4dd2133 Added new option "exit by pressing plus and minus buttons" to the input section. 2025-01-08 21:43:18 +10:00
Vladimir Sokolov
007d3bc045 Merge branch 'Ryubing:master' into master 2025-01-08 18:53:27 +10:00
Evan Husted
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
Evan Husted
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
Evan Husted
804d9c1efe docs: compat: remove invalid dupe 2025-01-07 06:03:35 -06:00
Evan Husted
9270b35648 no. 2025-01-07 05:53:31 -06:00
Evan Husted
5a6d01db3c docs: compat: trine 4 -> nothing
added Soul Reaver 1 & 2
2025-01-07 05:50:30 -06:00
Evan Husted
ef9c1416ec UI: compat: Only use monospaced font for title ID 2025-01-07 04:49:20 -06:00
Evan Husted
5efa7d5dfa UI: compat: remove custom ContentDialog derived type 2025-01-07 04:37:36 -06:00
Evan Husted
a82569d615 docs: compat: LEGO Horizon Adventures 2025-01-07 04:28:10 -06:00
Evan Husted
ed5832ca73 docs: compat: Add new releases to the end of the file 2025-01-07 04:21:33 -06:00
Evan Husted
574aa9ff9c add a couple missing title IDs 2025-01-07 04:21:08 -06:00
Evan Husted
8a29428de2 docs: compat: update hogwarts legacy compat 2025-01-07 03:57:13 -06:00
Evan Husted
f4272b05fa UI: Compat list disclaimer 2025-01-07 03:53:10 -06:00
Vladimir Sokolov
be2aa5ae8f Merge branch 'Ryubing:master' into master 2024-12-30 14:27:54 +10:00
Vladimir Sokolov
7f5978155b Merge branch 'GreemDev:master' into master 2024-12-24 20:59:34 +10:00
Vladimir Sokolov
cd43362042 Merge branch 'GreemDev:master' into master 2024-12-22 19:55:43 +10:00
Vova
39e8283fb8 Merge branch 'master' of https://github.com/Goodfeat/Ryujinx_alt 2024-12-15 09:33:41 +10:00
Vladimir Sokolov
3e3de18976 Merge branch 'GreemDev:master' into master 2024-11-30 11:01:14 +10:00
Vladimir Sokolov
a158a93d0a Merge branch 'GreemDev:master' into master 2024-11-25 09:10:46 +10:00
Vladimir Sokolov
ff90b68d09 Merge branch 'GreemDev:master' into master 2024-11-23 21:22:17 +10:00
Vladimir Sokolov
d7bd165861 Merge branch 'GreemDev:master' into master 2024-11-21 09:15:18 +10:00
Vladimir Sokolov
5bfa01b58a Merge branch 'GreemDev:master' into master 2024-11-13 09:45:58 +10:00
Vladimir Sokolov
7c30426f89 Merge branch 'GreemDev:master' into master 2024-11-10 15:42:43 +10:00
Vladimir Sokolov
267aaca87d Merge branch 'GreemDev:master' into master 2024-11-10 14:41:40 +10:00
Vova
b9012e291b code cleaning 2024-11-10 14:41:02 +10:00
Vladimir Sokolov
7da4a917ba Merge branch 'GreemDev:master' into master 2024-11-10 13:13:11 +10:00
Vladimir Sokolov
4bab858793 Merge branch 'GreemDev:master' into master 2024-11-09 20:21:57 +10:00
Vladimir Sokolov
ca7c17186e Merge branch 'GreemDev:master' into master 2024-11-04 20:47:13 +10:00
Vladimir Sokolov
ea061cf60c Merge branch 'GreemDev:master' into master 2024-11-03 18:55:10 +10:00
Vova
d83da7d2fb Fixed the logic of saving the input section. Added a new dialog box when changing parameters 2024-11-02 22:42:57 +10:00
Vladimir Sokolov
25499cbbf6 Merge branch 'GreemDev:master' into master 2024-11-02 22:38:33 +10:00
Vova
8074a4dd87 Fixed a visual bug in "input settings", when switching between players the settings were reset to default 2024-10-31 18:19:05 +10:00
Vova
13d2498405 test 2024-10-31 18:09:55 +10:00
68 changed files with 4052 additions and 4812 deletions

View File

@@ -3,16 +3,6 @@ name: Release job
on:
workflow_dispatch:
inputs: {}
push:
branches: [ release ]
paths-ignore:
- '.github/**'
- 'docs/**'
- 'assets/**'
- '*.yml'
- '*.json'
- '*.config'
- '*.md'
concurrency: release

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -284,7 +284,7 @@ namespace Ryujinx.HLE.HOS
ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
uint[] defaultCapabilities = {
0x030363F7,
(((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u,
0x1FFFFFCF,
0x207FFFEF,
0x47E0060F,

View File

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

View File

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

View File

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

View File

@@ -35,15 +35,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
DebuggingFlags &= ~3u;
KernelReleaseVersion = KProcess.KernelVersionPacked;
return Parse(capabilities, memoryManager);
return Parse(capabilities, memoryManager, false);
}
public Result InitializeForUser(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager)
public Result InitializeForUser(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager, bool isApplication)
{
return Parse(capabilities, memoryManager);
return Parse(capabilities, memoryManager, isApplication);
}
private Result Parse(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager)
private Result Parse(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager, bool isApplication)
{
int mask0 = 0;
int mask1 = 0;
@@ -54,7 +54,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
if (cap.GetCapabilityType() != CapabilityType.MapRange)
{
Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager);
Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager, isApplication);
if (result != Result.Success)
{
@@ -120,7 +120,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
return Result.Success;
}
private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager)
private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager, bool isApplication)
{
CapabilityType code = cap.GetCapabilityType();
@@ -176,6 +176,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
AllowedCpuCoresMask = GetMaskFromMinMax(lowestCpuCore, highestCpuCore);
AllowedThreadPriosMask = GetMaskFromMinMax(lowestThreadPrio, highestThreadPrio);
if (isApplication && lowestCpuCore == 0 && highestCpuCore != 2)
Ryujinx.Common.Logging.Logger.Error?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}! Report this to @LotP on the Ryujinx/Ryubing discord server (discord.gg/ryujinx)!");
else if (isApplication)
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}");
break;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -253,11 +253,23 @@ namespace Ryujinx.Input.SDL2
return IGamepad.GetStateSnapshot(this);
}
private static bool hotButtonMinus = false;
private static bool hotExit = false;
public bool SpecialExit()
{
if (hotButtonMinus)
{
hotButtonMinus = false;
return hotExit;
}
return hotExit = false;
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
GamepadStateSnapshot rawState = GetStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (_buttonsUserMapping.Count == 0)
@@ -270,6 +282,28 @@ namespace Ryujinx.Input.SDL2
if (!entry.IsValid)
continue;
if (GamepadButtonInputId.Minus == entry.To)
{
if (rawState.IsPressed(entry.From) && !hotButtonMinus)
{
hotButtonMinus = true;
}
else if (!result.IsPressed(entry.From) && hotButtonMinus)
{
hotButtonMinus = false;
}
}
if (GamepadButtonInputId.Plus == entry.To)
{
if (rawState.IsPressed(entry.To) && hotButtonMinus)
{
hotExit = true;
}
}
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
@@ -376,5 +410,7 @@ namespace Ryujinx.Input.SDL2
return SDL_GameControllerGetButton(_gamepadHandle, _buttonsDriverMapping[(int)inputId]) == 1;
}
}
}

View File

@@ -329,6 +329,11 @@ namespace Ryujinx.Input.SDL2
return result;
}
public bool SpecialExit()
{
return false;
}
public GamepadStateSnapshot GetStateSnapshot()
{
throw new NotSupportedException();

View File

@@ -25,6 +25,10 @@ namespace Ryujinx.Input.SDL2
{
_driver = driver;
}
public bool SpecialExit()
{
return false;
}
public Vector2 GetPosition()
{

View File

@@ -3,6 +3,7 @@ using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Collections.Concurrent;
@@ -273,15 +274,21 @@ namespace Ryujinx.Input.HLE
}
}
public void Update()
public bool Update()
{
// _gamepad may be altered by other threads
var gamepad = _gamepad;
if (gamepad != null && GamepadDriver != null)
{
State = gamepad.GetMappedStateSnapshot();
if (gamepad.SpecialExit())
{
return true;
}
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Motion.EnableMotion)
{
if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver)
@@ -334,6 +341,7 @@ namespace Ryujinx.Input.HLE
State = default;
_leftMotionInput = null;
}
return false;
}
public GamepadInput GetHLEInputState()

View File

@@ -200,8 +200,10 @@ namespace Ryujinx.Input.HLE
ReloadConfiguration(inputConfig, enableKeyboard, enableMouse);
}
public void Update(float aspectRatio = 1)
public bool Update(float aspectRatio = 1)
{
bool specialExit = false;
lock (_lock)
{
List<GamepadInput> hleInputStates = new();
@@ -225,9 +227,10 @@ namespace Ryujinx.Input.HLE
DriverConfigurationUpdate(ref controller, inputConfig);
controller.UpdateUserConfiguration(inputConfig);
controller.Update();
controller.UpdateRumble(_device.Hid.Npads.GetRumbleQueue(playerIndex));
specialExit = controller.Update(); //hotkey press check
controller.UpdateRumble(_device.Hid.Npads.GetRumbleQueue(playerIndex));
inputState = controller.GetHLEInputState();
inputState.Buttons |= _device.Hid.UpdateStickButtons(inputState.LStick, inputState.RStick);
@@ -315,6 +318,8 @@ namespace Ryujinx.Input.HLE
_device.TamperMachine.UpdateInput(hleInputStates);
}
return specialExit;
}
internal InputConfig GetPlayerInputConfigByIndex(int index)

View File

@@ -79,6 +79,12 @@ namespace Ryujinx.Input
/// <returns>A remapped snaphost of the state of the gamepad.</returns>
GamepadStateSnapshot GetMappedStateSnapshot();
/// <summary>
/// Gets the state if the minus and plus buttons were pressed on the gamepad.
/// </summary>
/// <returns>returns true if the buttons were pressed.</returns>
bool SpecialExit();
/// <summary>
/// Get a snaphost of the state of the gamepad.
/// </summary>

View File

@@ -18,6 +18,7 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Renderer;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Views.Main;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
@@ -70,6 +71,7 @@ namespace Ryujinx.Ava
private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
private const int TargetFps = 60;
private const float VolumeDelta = 0.05f;
static bool SpecialExit = false;
private static readonly Cursor _invisibleCursor = new(StandardCursorType.None);
private readonly nint _invisibleCursorWin;
@@ -96,6 +98,7 @@ namespace Ryujinx.Ava
private bool _isCursorInRenderer = true;
private bool _ignoreCursorState = false;
private enum CursorStates
{
CursorIsHidden,
@@ -503,10 +506,15 @@ namespace Ryujinx.Ava
_viewModel.Volume = ConfigurationState.Instance.System.AudioVolume.Value;
MainLoop();
Exit();
}
public bool IsSpecialExit()
{
return SpecialExit;
}
private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs<bool> args)
{
if (Device != null)
@@ -589,6 +597,7 @@ namespace Ryujinx.Ava
_isStopped = true;
Stop();
}
public void DisposeContext()
@@ -1135,6 +1144,7 @@ namespace Ryujinx.Ava
string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld];
string vSyncMode = Device.VSyncMode.ToString();
UpdateShaderCount();
if (GraphicsConfig.ResScale != 1)
@@ -1200,7 +1210,17 @@ namespace Ryujinx.Ava
return false;
}
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()))
{
if (ConfigurationState.Instance.Hid.SpecialExitEmulator.Value == 1)
{
SpecialExit = true; // close App
}
if (ConfigurationState.Instance.Hid.SpecialExitEmulator.Value > 0)
{
_isActive = false; //close game
}
}
if (_viewModel.IsActive)
{
@@ -1335,6 +1355,8 @@ namespace Ryujinx.Ava
Device.Hid.DebugPad.Update();
return true;
}

View File

@@ -402,7 +402,7 @@
<x:Double x:Key="ControlContentThemeFontSize">13</x:Double>
<x:Double x:Key="MenuItemHeight">26</x:Double>
<x:Double x:Key="TabItemMinHeight">28</x:Double>
<x:Double x:Key="ContentDialogMaxWidth">700</x:Double>
<x:Double x:Key="ContentDialogMaxWidth">900</x:Double>
<x:Double x:Key="ContentDialogMaxHeight">756</x:Double>
</Styles.Resources>
</Styles>

View File

@@ -147,6 +147,31 @@
"zh_TW": "滑鼠直接存取"
}
},
{
"ID": "SettingsExtraCloseApp",
"Translations": {
"ar_SA": "خروج سريع من التطبيق",
"de_DE": "Schneller Ausstieg aus der Anwendung",
"el_GR": "Γρήγορη έξοδος από την εφαρμογή",
"en_US": "Quick Exit from Application",
"es_ES": "Salida rápida de la aplicación",
"fr_FR": "Sortie rapide de l'application",
"he_IL": "יציאה מהירה מהאפליקציה",
"it_IT": "Uscita rapida dall'applicazione",
"ja_JP": "アプリケーションからの迅速な終了",
"ko_KR": "애플리케이션에서 빠른 종료",
"no_NO": "Rask avslutning av applikasjonen",
"pl_PL": "Szybkie wyjście z aplikacji",
"pt_BR": "Saída rápida do aplicativo",
"ru_RU": "Быстрый выход из приложения",
"sv_SE": "Snabb avslutning från applikationen",
"th_TH": "ออกจากแอปพลิเคชันอย่างรวดเร็ว",
"tr_TR": "Uygulamadan Hızlı Çıkış",
"uk_UA": "Швидкий вихід з програми",
"zh_CN": "快速退出应用程序",
"zh_TW": "快速退出應用程式"
}
},
{
"ID": "SettingsTabSystemMemoryManagerMode",
"Translations": {
@@ -11047,6 +11072,81 @@
"zh_TW": "淺色"
}
},
{
"ID": "SettingsTabInputDisableExitHotKey",
"Translations": {
"ar_SA": "الخروج السريع معطل",
"de_DE": "Schneller Ausstieg deaktiviert",
"el_GR": "Η γρήγορη έξοδος είναι απενεργοποιημένη",
"en_US": "Quick exit disabled",
"es_ES": "Salida rápida desactivada",
"fr_FR": "Sortie rapide désactivée",
"he_IL": "יציאה מהירה מושבתת",
"it_IT": "Uscita rapida disabilitata",
"ja_JP": "クイック終了が無効です",
"ko_KR": "빠른 종료 비활성화됨",
"no_NO": "Rask avslutning er deaktivert",
"pl_PL": "Szybkie wyjście wyłączone",
"pt_BR": "Saída rápida desativada",
"ru_RU": "Быстрый выход выключен",
"sv_SE": "Snabb avslutning inaktiverad",
"th_TH": "ปิดใช้งานออกอย่างรวดเร็ว",
"tr_TR": "Hızlı çıkış devre dışı bırakıldı",
"uk_UA": "Швидкий вихід вимкнено",
"zh_CN": "快速退出已禁用",
"zh_TW": "快速退出已停用"
}
},
{
"ID": "SettingsTabInputHotkeyIsCloseApp",
"Translations": {
"ar_SA": "إغلاق التطبيق بالضغط على الزرين '+' و '-'.",
"de_DE": "App schließen mit den '+' und '-' Tasten.",
"el_GR": "Κλείσιμο της εφαρμογής με τα κουμπιά '+' και '-'.",
"en_US": "Close app by '+' and '-' buttons.",
"es_ES": "Cerrar la aplicación pulsando los botones '+' y '-'.",
"fr_FR": "Fermer l'application avec les boutons '+' et '-'.",
"he_IL": "סגור את האפליקציה בלחיצה על '+' ו-'-'.",
"it_IT": "Chiudi l'app premendo i pulsanti '+' e '-'.",
"ja_JP": "「+」と「-」ボタンを押してアプリを終了します。",
"ko_KR": "'+' 및 '-' 버튼을 눌러 앱을 종료합니다.",
"no_NO": "Lukk appen med '+' og '-' knappene.",
"pl_PL": "Zamknij aplikację przyciskiem '+' i '-'.",
"pt_BR": "Fechar o aplicativo pressionando os botões '+' e '-'.",
"ru_RU": "Закрыть приложение нажатием '+' и '-'.",
"sv_SE": "Stäng appen med '+' och '-' knapparna.",
"th_TH": "ปิดแอปโดยกดปุ่ม '+' และ '-'.",
"tr_TR": "'+' ve '-' düğmelerine basarak uygulamayı kapatın.",
"uk_UA": "Закрити додаток натисканням '+' та '-'.",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "SettingsTabInputHotkeyIsCloseGame",
"Translations": {
"ar_SA": "الخروج من اللعبة بالضغط على الزرين '+' و '-'.",
"de_DE": "Spiel beenden mit den '+' und '-' Tasten.",
"el_GR": "Έξοδος από το παιχνίδι με τα κουμπιά '+' και '-'.",
"en_US": "Exit game by '+' and '-' buttons.",
"es_ES": "Salir del juego pulsando los botones '+' y '-'.",
"fr_FR": "Quitter le jeu avec les boutons '+' et '-'.",
"he_IL": "יציאה מהמשחק בלחיצה על '+' ו-'-'.",
"it_IT": "Esci dal gioco premendo i pulsanti '+' e '-'.",
"ja_JP": "「+」と「-」ボタンを押してゲームを終了します。",
"ko_KR": "'+' 및 '-' 버튼을 눌러 게임을 종료합니다.",
"no_NO": "Avslutt spillet med '+' og '-' knappene.",
"pl_PL": "Wyjście z gry przyciskiem '+' i '-'.",
"pt_BR": "Sair do jogo pressionando os botões '+' e '-'.",
"ru_RU": "Выйти из игры нажатием '+' и '-'.",
"sv_SE": "Avsluta spelet med '+' och '-' knapparna.",
"th_TH": "ออกจากเกมโดยกดปุ่ม '+' และ '-'.",
"tr_TR": "'+' ve '-' düğmelerine basarak oyundan çıkın.",
"uk_UA": "Вийти з гри натисканням '+' та '-'.",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "ControllerSettingsConfigureGeneral",
"Translations": {
@@ -15147,6 +15247,31 @@
"zh_TW": "支援滑鼠直接存取 (HID)。遊戲可將滑鼠作為指向裝置使用。\n\n僅適用於在 Switch 硬體上原生支援滑鼠控制的遊戲,這類遊戲很少。\n\n啟用後觸控螢幕功能可能無法使用。\n\n如果不確定請保持關閉狀態。"
}
},
{
"ID": "SpecialExitTooltip",
"Translations": {
"ar_SA": "يقوم بتفعيل مفاتيح الاختصار 'زائد' و 'ناقص'.\nاضغط على زرّي زائد وناقص في نفس الوقت لتنفيذ إحدى العمليات:\n\n1) إغلاق التطبيق باستخدام مفاتيح الاختصار.\n2) الخروج من اللعبة دون إغلاق التطبيق.\n\nيعمل فقط مع أذرع التحكم.",
"de_DE": "Aktiviert die Hotkeys 'Plus' und 'Minus'.\nDrücken Sie gleichzeitig die Plus- und Minus-Tasten, um eine der folgenden Aktionen auszuführen:\n\n1) Schließt die Anwendung durch Drücken der Hotkeys.\n2) Beendet das Spiel, ohne die Anwendung zu schließen.\n\nFunktioniert nur mit Gamepads.",
"el_GR": "Ενεργοποιεί τα πλήκτρα πρόσβασης 'συν' και 'πλην'.\nΠατήστε ταυτόχρονα τα κουμπιά συν και πλην για μία από τις ενέργειες:\n\n1) Κλείνει την εφαρμογή πατώντας τα πλήκτρα πρόσβασης.\n2) Εξέρχεται από το παιχνίδι χωρίς να κλείσει η εφαρμογή.\n\nΛειτουργεί μόνο με gamepads.",
"en_US": "Activates the hot keys 'plus' and 'minus'.\nPress buttons plus and minus at the same time to get one of the actions:\n\n1) Closes the application by pressing the hot keys.\n2) Exits the game without closing the application.\n\nWorks only with gamepads.",
"es_ES": "Activa las teclas rápidas 'más' y 'menos'.\nPresiona los botones más y menos al mismo tiempo para realizar una de las siguientes acciones:\n\n1) Cierra la aplicación presionando las teclas rápidas.\n2) Salir del juego sin cerrar la aplicación.\n\nFunciona solo con mandos.",
"fr_FR": "Active les raccourcis 'plus' et 'moins'.\nAppuyez simultanément sur les boutons plus et moins pour effectuer l'une des actions suivantes :\n\n1) Ferme l'application en appuyant sur les raccourcis.\n2) Quitte le jeu sans fermer l'application.\n\nFonctionne uniquement avec les manettes.",
"he_IL": "מפעיל את המקשים הקצרים 'פלוס' ו-'מינוס'.\nלחץ על הכפתורים פלוס ומינוס בו זמנית כדי לבצע אחת מהפעולות:\n\n1) סוגר את היישום באמצעות המקשים הקצרים.\n2) יוצא מהמשחק מבלי לסגור את היישום.\n\nפועל רק עם בקרי משחק.",
"it_IT": "Attiva i tasti rapidi 'più' e 'meno'.\nPremere i pulsanti più e meno contemporaneamente per eseguire una delle seguenti azioni:\n\n1) Chiude l'applicazione premendo i tasti rapidi.\n2) Esce dal gioco senza chiudere l'applicazione.\n\nFunziona solo con i gamepad.",
"ja_JP": "ホットキー「プラス」と「マイナス」を有効化します。\nプラスとマイナスのボタンを同時に押して、次のいずれかの操作を実行します:\n\n1) ホットキーを押すことでアプリを閉じます。\n2) アプリを閉じずにゲームを終了します。\n\nゲームパッドでのみ動作します。",
"ko_KR": "'플러스' 및 '마이너스' 단축키를 활성화합니다.\n플러스 및 마이너스 버튼을 동시에 눌러 다음 작업 중 하나를 수행합니다:\n\n1) 단축키를 눌러 애플리케이션을 닫습니다.\n2) 애플리케이션을 닫지 않고 게임을 종료합니다.\n\n게임패드에서만 작동합니다.",
"no_NO": "Aktiverer hurtigtastene 'pluss' og 'minus'.\nTrykk på knappene pluss og minus samtidig for å utføre en av følgende handlinger:\n\n1) Lukker applikasjonen ved å trykke på hurtigtastene.\n2) Avslutter spillet uten å lukke applikasjonen.\n\nFungerer kun med spillkontroller.",
"pl_PL": "Aktywuje klawisze skrótu 'plus' i 'minus'.\nNaciśnij jednocześnie przyciski plus i minus, aby wykonać jedną z akcji:\n\n1) Zamknij aplikację, naciskając klawisze skrótu.\n2) Wyjdź z gry bez zamykania aplikacji.\n\nDziała tylko z gamepadami.",
"pt_BR": "Ativa as teclas de atalho 'mais' e 'menos'.\nPressione os botões mais e menos ao mesmo tempo para realizar uma das ações:\n\n1) Fecha o aplicativo pressionando as teclas de atalho.\n2) Sai do jogo sem fechar o aplicativo.\n\nFunciona apenas com gamepads.",
"ru_RU": "Активирует горячие клавиши 'плюс' и 'минус'.\nНажмите одновременно кнопки плюс и минус чтобы получить одно из действий:\n\n1) Закрывает приложение по нажатию горячих кнопок.\n2) Выходит из игры без закрытия приложения.\n\nРаботает только с геймпадами.",
"sv_SE": "Aktiverar snabbtangenterna 'plus' och 'minus'.\nTryck samtidigt på plus- och minusknapparna för att utföra en av följande åtgärder:\n\n1) Stänger applikationen med snabbtangenterna.\n2) Avslutar spelet utan att stänga applikationen.\n\nFungerar bara med spelkontroller.",
"th_TH": "เปิดใช้งานปุ่มลัด '+' และ '-'.\nกดปุ่ม '+' และ '-' พร้อมกันเพื่อทำการอย่างใดอย่างหนึ่ง:\n\n1) ปิดแอปพลิเคชันด้วยการกดปุ่มลัด\n2) ออกจากเกมโดยไม่ปิดแอปพลิเคชัน\n\nใช้งานได้เฉพาะกับจอยเกม",
"tr_TR": "'Artı' ve 'eksi' kısayol tuşlarını etkinleştirir.\nArtı ve eksi tuşlarına aynı anda basarak aşağıdaki işlemlerden birini gerçekleştirin:\n\n1) Kısayol tuşlarına basarak uygulamayı kapatır.\n2) Uygulamayı kapatmadan oyundan çıkar.\n\nYalnızca gamepad'lerle çalışır.",
"uk_UA": "Активує гарячі клавіші '+' та '-'.\nНатисніть одночасно кнопки плюс та мінус для виконання однієї з дій:\n\n1) Закриває додаток за допомогою гарячих клавіш.\n2) Виходить із гри без закриття додатка.\n\nПрацює лише з геймпадами.",
"zh_CN": "激活快捷键“加号”和“减号”。\n同时按下加号和减号按钮以执行以下操作之一\n\n1) 按快捷键关闭应用程序。\n2) 退出游戏而不关闭应用程序。\n\n仅适用于游戏手柄。",
"zh_TW": "啟用快捷鍵「加號」和「減號」。\n同時按下加號和減號按鈕以執行以下其中一項操作\n\n1) 按快捷鍵關閉應用程式。\n2) 離開遊戲而不關閉應用程式。\n\n僅適用於遊戲手柄。"
}
},
{
"ID": "RegionTooltip",
"Translations": {
@@ -22597,6 +22722,56 @@
"zh_TW": ""
}
},
{
"ID": "CompatibilityListLastUpdated",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Last updated: {0}",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "CompatibilityListWarning",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "This compatibility list might contain out of date entries.\nDo not be opposed to testing games in the \"Ingame\" status.",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "CompatibilityListSearchBoxWatermark",
"Translations": {

View File

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

View File

@@ -350,11 +350,11 @@ namespace Ryujinx.Headless
{
return options.GraphicsBackend switch
{
GraphicsBackend.Vulkan => new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet),
GraphicsBackend.Vulkan => new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet, options.SpecialExit),
GraphicsBackend.Metal => OperatingSystem.IsMacOS() ?
new MetalWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableKeyboard, options.HideCursorMode, options.IgnoreControllerApplet) :
new MetalWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableKeyboard, options.HideCursorMode, options.IgnoreControllerApplet, options.SpecialExit) :
throw new Exception("Attempted to use Metal renderer on non-macOS platform!"),
_ => new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet)
_ => new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet, options.SpecialExit)
};
}

View File

@@ -23,8 +23,9 @@ namespace Ryujinx.Headless
AspectRatio aspectRatio,
bool enableMouse,
HideCursorMode hideCursorMode,
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { }
bool ignoreControllerApplet,
int specialExitEmulator)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet, specialExitEmulator) { }
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_METAL;

View File

@@ -118,8 +118,9 @@ namespace Ryujinx.Headless
AspectRatio aspectRatio,
bool enableMouse,
HideCursorMode hideCursorMode,
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
bool ignoreControllerApplet,
int specialExitEmulator)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet, specialExitEmulator)
{
_glLogLevel = glLogLevel;
}

View File

@@ -150,7 +150,10 @@ namespace Ryujinx.Headless
if (NeedsOverride(nameof(IgnoreControllerApplet)))
IgnoreControllerApplet = configurationState.IgnoreApplet;
if (NeedsOverride(nameof(SpecialExit)))
SpecialExit = configurationState.Hid.SpecialExitEmulator;
return;
bool NeedsOverride(string argKey) => originalArgs.None(arg => arg.TrimStart('-').EqualsIgnoreCase(OptionName(argKey)));
@@ -274,6 +277,9 @@ namespace Ryujinx.Headless
[Option("enable-mouse", Required = false, Default = false, HelpText = "Enable or disable mouse support.")]
public bool EnableMouse { get; set; }
[Option("enable-press-hotkeys-to-exit", Required = false, Default = 0, HelpText = "press the minus and plus buttons to: 0 -disable, 1 - exit app, 2 - exit game.")]
public int SpecialExit { get; set; }
[Option("hide-cursor", Required = false, Default = HideCursorMode.OnIdle, HelpText = "Change when the cursor gets hidden.")]
public HideCursorMode HideCursorMode { get; set; }
@@ -414,6 +420,7 @@ namespace Ryujinx.Headless
[Option("ignore-controller-applet", Required = false, Default = false, HelpText = "Enable ignoring the controller applet when your game loses connection to your controller.")]
public bool IgnoreControllerApplet { get; set; }
// Values
[Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)]

View File

@@ -18,8 +18,9 @@ namespace Ryujinx.Headless
AspectRatio aspectRatio,
bool enableMouse,
HideCursorMode hideCursorMode,
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
bool ignoreControllerApplet,
int specialExitEmulator)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet, specialExitEmulator)
{
_glLogLevel = glLogLevel;
}

View File

@@ -88,6 +88,7 @@ namespace Ryujinx.Headless
private readonly AspectRatio _aspectRatio;
private readonly bool _enableMouse;
private readonly int _specialExitEmulator;
private readonly bool _ignoreControllerApplet;
public WindowBase(
@@ -96,7 +97,8 @@ namespace Ryujinx.Headless
AspectRatio aspectRatio,
bool enableMouse,
HideCursorMode hideCursorMode,
bool ignoreControllerApplet)
bool ignoreControllerApplet,
int specialExitEmulator)
{
MouseDriver = new SDL2MouseDriver(hideCursorMode);
_inputManager = inputManager;
@@ -112,6 +114,7 @@ namespace Ryujinx.Headless
_gpuDoneEvent = new ManualResetEvent(false);
_aspectRatio = aspectRatio;
_enableMouse = enableMouse;
_specialExitEmulator = specialExitEmulator;
_ignoreControllerApplet = ignoreControllerApplet;
HostUITheme = new HeadlessHostUiTheme();

View File

@@ -30,6 +30,11 @@ namespace Ryujinx.Ava.Input
public readonly Key From = from;
}
public bool SpecialExit()
{
return false;
}
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name)
{
_buttonsUserMapping = [];

View File

@@ -13,6 +13,11 @@ namespace Ryujinx.Ava.Input
public string Id => "0";
public string Name => "AvaloniaMouse";
public bool SpecialExit()
{
return false;
}
public bool IsConnected => true;
public GamepadFeaturesFlag Features => throw new NotImplementedException();
public bool[] Buttons => _driver.PressedButtons;

View File

@@ -171,12 +171,6 @@
<ItemGroup>
<AdditionalFiles Include="Assets\locales.json" />
</ItemGroup>
<ItemGroup>
<Compile Update="Utilities\Compat\CompatibilityContentDialog.axaml.cs">
<DependentUpon>CompatibilityContentDialog.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\Fonts\Mono\" />
</ItemGroup>

View File

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

View File

@@ -12,8 +12,8 @@ namespace Ryujinx.Ava.UI.Helpers
private static readonly Lazy<PlayabilityStatusConverter> _shared = new(() => new());
public static PlayabilityStatusConverter Shared => _shared.Value;
public object Convert(object? value, Type _, object? __, CultureInfo ___) =>
value.Cast<LocaleKeys>() switch
public object Convert(object value, Type _, object __, CultureInfo ___)
=> value.Cast<LocaleKeys>() switch
{
LocaleKeys.CompatibilityListNothing or
LocaleKeys.CompatibilityListBoots or
@@ -22,7 +22,7 @@ namespace Ryujinx.Ava.UI.Helpers
_ => Brushes.ForestGreen
};
public object ConvertBack(object? value, Type _, object? __, CultureInfo ___)
public object ConvertBack(object value, Type _, object __, CultureInfo ___)
=> throw new NotSupportedException();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -741,7 +741,10 @@ namespace Ryujinx.Ava.UI.ViewModels
Applications.ToObservableChangeSet()
.Filter(Filter)
.Sort(GetComparer())
.Bind(out _appsObservableList).AsObservableList();
#pragma warning disable MVVMTK0034
.Bind(out _appsObservableList)
#pragma warning restore MVVMTK0034
.AsObservableList();
OnPropertyChanged(nameof(AppsObservableList));
}
@@ -1046,6 +1049,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private void InitializeGame()
{
RendererHostControl.WindowCreated += RendererHost_Created;
AppHost.StatusUpdatedEvent += Update_StatusBar;
@@ -1055,7 +1059,13 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost?.Start();
if (AppHost?.IsSpecialExit() == true)
{
Window.ForceExit();
}
AppHost?.DisposeContext();
}
private async Task HandleRelaunch()

View File

@@ -128,6 +128,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableDockedMode { get; set; }
public bool EnableKeyboard { get; set; }
public bool EnableMouse { get; set; }
public int EnableSpecialExit { get; set; }
public VSyncMode VSyncMode
{
get => _vSyncMode;
@@ -259,6 +260,8 @@ namespace Ryujinx.Ava.UI.ViewModels
public int OpenglDebugLevel { get; set; }
public int MemoryMode { get; set; }
public int BaseStyleIndex { get; set; }
public int GraphicsBackendIndex
{
get => _graphicsBackendIndex;
@@ -511,6 +514,13 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableDockedMode = config.System.EnableDockedMode;
EnableKeyboard = config.Hid.EnableKeyboard;
EnableMouse = config.Hid.EnableMouse;
EnableSpecialExit = config.Hid.SpecialExitEmulator.Value switch
{
0 => 0, // "Hotkey 'Exit' is Disabled"
1 => 1, // "Close app. by hotkey"
2 => 2, // "Close game by hotkey"
_ => 0
};
// Keyboard Hotkeys
KeyboardHotkey = new HotkeyConfig(config.Hid.Hotkeys.Value);
@@ -618,6 +628,13 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.EnableDockedMode.Value = EnableDockedMode;
config.Hid.EnableKeyboard.Value = EnableKeyboard;
config.Hid.EnableMouse.Value = EnableMouse;
config.Hid.SpecialExitEmulator.Value = EnableSpecialExit switch
{
0 => 0, // "Hotkey 'Exit' is Disabled",
1 => 1, // "Close app. by hotkey",
2 => 2, // "Close game by hotkey",
_ => 0
};
// Keyboard Hotkeys
config.Hid.Hotkeys.Value = KeyboardHotkey.GetConfig();

View File

@@ -1,4 +1,4 @@
<UserControl
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsInputView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -58,6 +58,20 @@
<TextBlock
Text="{ext:Locale SettingsTabInputDirectMouseAccess}" />
</CheckBox>
<ComboBox SelectedIndex="{Binding EnableSpecialExit}"
ToolTip.Tip="{ext:Locale SpecialExitTooltip}"
HorizontalContentAlignment="Left"
MinWidth="160">
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabInputDisableExitHotKey}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabInputHotkeyIsCloseApp}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabInputHotkeyIsCloseGame}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
</StackPanel>
</Grid>

View File

@@ -45,6 +45,7 @@ namespace Ryujinx.Ava.UI.Windows
internal readonly AvaHostUIHandler UiHandler;
private bool _isLoading;
private bool _isExitWithoutConfirm = false;
private bool _applicationsLoadedOnce;
private UserChannelPersistence _userChannelPersistence;
@@ -164,7 +165,7 @@ namespace Ryujinx.Ava.UI.Windows
});
}
private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e)
private void ApplicationLibrary_LdnGameDataReceived(LdnGameDataReceivedEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
@@ -408,13 +409,10 @@ namespace Ryujinx.Ava.UI.Windows
{
StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged;
ApplicationGrid.DataContext = ApplicationList.DataContext = ViewModel;
ApplicationGrid.ApplicationOpened += Application_Opened;
ApplicationGrid.DataContext = ViewModel;
ApplicationList.ApplicationOpened += Application_Opened;
ApplicationList.DataContext = ViewModel;
}
private void SetWindowSizePosition()
@@ -574,11 +572,11 @@ namespace Ryujinx.Ava.UI.Windows
protected override void OnClosing(WindowClosingEventArgs e)
{
if (!ViewModel.IsClosing && ViewModel.AppHost != null && ConfigurationState.Instance.ShowConfirmExit)
if (!ViewModel.IsClosing && ViewModel.AppHost != null && ConfigurationState.Instance.ShowConfirmExit && !_isExitWithoutConfirm)
{
e.Cancel = true;
ConfirmExit();
ConfirmExit();
return;
}
@@ -619,6 +617,12 @@ namespace Ryujinx.Ava.UI.Windows
base.OnClosing(e);
}
public void ForceExit()
{
_isExitWithoutConfirm = true;
Close();
}
private void ConfirmExit()
{
Dispatcher.UIThread.InvokeAsync(async () =>

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public const string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
public Language DesiredLanguage { get; set; }
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
public event EventHandler<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
public event Action<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
public readonly IObservableCache<ApplicationData, ulong> Applications;
public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates;
@@ -779,7 +779,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
using HttpClient httpClient = new HttpClient();
string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games");
ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData);
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{
LdnData = ldnGameDataArray
});
@@ -787,7 +787,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
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}");
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{
LdnData = Array.Empty<LdnGameData>()
});
@@ -795,7 +795,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
}
else
{
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{
LdnData = Array.Empty<LdnGameData>()
});

View File

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

View File

@@ -1,20 +0,0 @@
<ui:ContentDialog xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="using:Ryujinx.Ava.Utilities.Compat"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
x:Class="Ryujinx.Ava.Utilities.Compat.CompatibilityContentDialog"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400"
CloseButtonText="{ext:Locale SettingsButtonClose}"
DefaultButton="Close"
x:DataType="local:CompatibilityViewModel">
<ui:ContentDialog.DataContext>
<local:CompatibilityViewModel/>
</ui:ContentDialog.DataContext>
<ui:ContentDialog.Resources>
<x:Double x:Key="ContentDialogMaxWidth">900</x:Double>
</ui:ContentDialog.Resources>
</ui:ContentDialog>

View File

@@ -1,13 +0,0 @@
using FluentAvalonia.UI.Controls;
using System;
namespace Ryujinx.Ava.Utilities.Compat
{
public partial class CompatibilityContentDialog : ContentDialog
{
protected override Type StyleKeyOverride => typeof(ContentDialog);
public CompatibilityContentDialog() => InitializeComponent();
}
}

View File

@@ -1,52 +1,66 @@
using Gommon;
using Humanizer;
using nietras.SeparatedValues;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
namespace Ryujinx.Ava.Utilities.Compat
{
public struct ColumnIndices(Func<ReadOnlySpan<char>, int> getIndex)
{
public const string TitleIdCol = "\"title_id\"";
public const string GameNameCol = "\"game_name\"";
public const string LabelsCol = "\"labels\"";
public const string StatusCol = "\"status\"";
public const string LastUpdatedCol = "\"last_updated\"";
public readonly int TitleId = getIndex(TitleIdCol);
public readonly int GameName = getIndex(GameNameCol);
public readonly int Labels = getIndex(LabelsCol);
public readonly int Status = getIndex(StatusCol);
public readonly int LastUpdated = getIndex(LastUpdatedCol);
}
public class CompatibilityCsv
{
public static CompatibilityCsv Shared { get; set; }
public CompatibilityCsv(SepReader reader)
static CompatibilityCsv()
{
var entries = new List<CompatibilityEntry>();
using Stream csvStream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("RyujinxGameCompatibilityList")!;
csvStream.Position = 0;
foreach (var row in reader)
{
entries.Add(new CompatibilityEntry(reader.Header, row));
}
using SepReader reader = Sep.Reader().From(csvStream);
ColumnIndices columnIndices = new(reader.Header.IndexOf);
Entries = entries.Where(x => x.Status != null)
.OrderBy(it => it.GameName).ToArray();
Entries = reader
.Enumerate(row => new CompatibilityEntry(ref columnIndices, row))
.OrderBy(it => it.GameName)
.ToArray();
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatCsv");
}
public CompatibilityEntry[] Entries { get; }
public static CompatibilityEntry[] Entries { get; private set; }
}
public class CompatibilityEntry
{
public CompatibilityEntry(SepReaderHeader header, SepReader.Row row)
public CompatibilityEntry(ref ColumnIndices indices, SepReader.Row row)
{
IssueNumber = row[header.IndexOf("issue_number")].Parse<int>();
var titleIdRow = row[header.IndexOf("extracted_game_id")].ToString();
string titleIdRow = ColStr(row[indices.TitleId]);
TitleId = !string.IsNullOrEmpty(titleIdRow)
? titleIdRow
: default(Optional<string>);
GameName = ColStr(row[indices.GameName]);
var issueTitleRow = row[header.IndexOf("issue_title")].ToString();
if (TitleId.HasValue)
issueTitleRow = issueTitleRow.ReplaceIgnoreCase($" - {TitleId}", string.Empty);
GameName = issueTitleRow.Trim().Trim('"');
IssueLabels = row[header.IndexOf("issue_labels")].ToString().Split(';');
Status = row[header.IndexOf("extracted_status")].ToString().ToLower() switch
Labels = ColStr(row[indices.Labels]).Split(';');
Status = ColStr(row[indices.Status]).ToLower() switch
{
"playable" => LocaleKeys.CompatibilityListPlayable,
"ingame" => LocaleKeys.CompatibilityListIngame,
@@ -56,39 +70,41 @@ namespace Ryujinx.Ava.Utilities.Compat
_ => null
};
if (row[header.IndexOf("last_event_date")].TryParse<DateTime>(out var dt))
LastEvent = dt;
if (DateTime.TryParse(ColStr(row[indices.LastUpdated]), out var dt))
LastUpdated = dt;
if (row[header.IndexOf("events_count")].TryParse<int>(out var eventsCount))
EventCount = eventsCount;
return;
string ColStr(SepReader.Col col) => col.ToString().Trim('"');
}
public int IssueNumber { get; }
public string GameName { get; }
public Optional<string> TitleId { get; }
public string[] IssueLabels { get; }
public string[] Labels { get; }
public LocaleKeys? Status { get; }
public DateTime LastEvent { get; }
public int EventCount { get; }
public DateTime LastUpdated { get; }
public string LocalizedLastUpdated =>
LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
public string LocalizedStatus => LocaleManager.Instance[Status!.Value];
public string FormattedTitleId => TitleId.OrElse(new string(' ', 16));
public string FormattedTitleId => TitleId
.OrElse(new string(' ', 16));
public string FormattedIssueLabels => IssueLabels
.Where(it => !it.StartsWithIgnoreCase("status"))
public string FormattedIssueLabels => Labels
.Select(FormatLabelName)
.JoinToString(", ");
public override string ToString()
{
var sb = new StringBuilder("CompatibilityEntry: {");
sb.Append($"{nameof(IssueNumber)}={IssueNumber}, ");
StringBuilder sb = new("CompatibilityEntry: {");
sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
sb.Append($"{nameof(TitleId)}={TitleId}, ");
sb.Append($"{nameof(IssueLabels)}=\"{IssueLabels}\", ");
sb.Append($"{nameof(Labels)}={
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
}, ");
sb.Append($"{nameof(Status)}=\"{Status}\", ");
sb.Append($"{nameof(LastEvent)}=\"{LastEvent}\", ");
sb.Append($"{nameof(EventCount)}={EventCount}");
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
sb.Append('}');
return sb.ToString();
@@ -144,8 +160,8 @@ namespace Ryujinx.Ava.Utilities.Compat
if (value == string.Empty)
return string.Empty;
var firstChar = value[0];
var rest = value[1..];
char firstChar = value[0];
string rest = value[1..];
return $"{char.ToUpper(firstChar)}{rest}";
}

View File

@@ -4,6 +4,7 @@
xmlns:local="using:Ryujinx.Ava.Utilities.Compat"
xmlns:helpers="using:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.Utilities.Compat.CompatibilityList"
@@ -11,24 +12,46 @@
<UserControl.DataContext>
<local:CompatibilityViewModel />
</UserControl.DataContext>
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto">
<Grid RowDefinitions="*,Auto,*">
<Grid
Grid.Row="0"
HorizontalAlignment="Center"
ColumnDefinitions="Auto,*"
Margin="0 0 0 10">
<ui:FontIcon
Grid.Column="0"
Margin="0"
HorizontalAlignment="Stretch"
FontFamily="avares://FluentAvalonia/Fonts#Symbols"
Glyph="{helpers:GlyphValueConverter Important}" />
<!-- NOTE: aligning to bottom for better visual alignment with glyph -->
<TextBlock
Grid.Column="1"
Margin="5, 0, 0, 0"
FontStyle="Italic"
VerticalAlignment="Center"
TextWrapping="Wrap"
Text="{ext:Locale CompatibilityListWarning}" />
</Grid>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto">
<TextBox Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{ext:Locale CompatibilityListSearchBoxWatermark}" TextChanged="TextBox_OnTextChanged" />
<CheckBox Grid.Column="1" Margin="7, 0, 0, 0" IsChecked="{Binding OnlyShowOwnedGames}" />
<TextBlock Grid.Column="2" Margin="-10, 0, 0, 0" Text="{ext:Locale CompatibilityListOnlyShowOwnedGames}" />
</Grid>
<ScrollViewer Grid.Row="1">
<ScrollViewer Grid.Row="2">
<ListBox Margin="0,5, 0, 0"
Background="Transparent"
ItemsSource="{Binding CurrentEntries}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:CompatibilityEntry}">
<Grid Width="750" ColumnDefinitions="Auto,Auto,Auto,*"
Margin="5">
<Grid Width="750"
Margin="5"
ColumnDefinitions="Auto,Auto,Auto,*"
Background="Transparent"
ToolTip.Tip="{Binding LocalizedLastUpdated}">
<TextBlock Grid.Column="0"
FontFamily="{StaticResource JetBrainsMono}"
Text="{Binding GameName}"
Width="333"
Width="320"
TextWrapping="Wrap" />
<TextBlock Grid.Column="1"
Width="135"
@@ -39,14 +62,12 @@
<TextBlock Grid.Column="2"
Padding="7, 0"
VerticalAlignment="Center"
FontFamily="{StaticResource JetBrainsMono}"
Text="{Binding LocalizedStatus}"
Width="85"
Foreground="{Binding Status, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextWrapping="NoWrap" />
<TextBlock Grid.Column="3"
VerticalAlignment="Center"
FontFamily="{StaticResource JetBrainsMono}"
Text="{Binding FormattedIssueLabels}"
TextWrapping="WrapWithOverflow" />
</Grid>

View File

@@ -1,6 +1,8 @@
using Avalonia.Controls;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using nietras.SeparatedValues;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using System.IO;
using System.Reflection;
@@ -12,18 +14,15 @@ namespace Ryujinx.Ava.Utilities.Compat
{
public static async Task Show()
{
if (CompatibilityCsv.Shared is null)
ContentDialog contentDialog = new()
{
await using Stream csvStream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("RyujinxGameCompatibilityList")!;
csvStream.Position = 0;
CompatibilityCsv.Shared = new CompatibilityCsv(Sep.Reader().From(csvStream));
}
CompatibilityContentDialog contentDialog = new()
{
Content = new CompatibilityList { DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary) }
PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
Content = new CompatibilityList
{
DataContext = new CompatibilityViewModel(RyujinxApp.MainWindow.ViewModel.ApplicationLibrary)
}
};
Style closeButton = new(x => x.Name("CloseButton"));
@@ -43,7 +42,7 @@ namespace Ryujinx.Ava.Utilities.Compat
InitializeComponent();
}
private void TextBox_OnTextChanged(object? sender, TextChangedEventArgs e)
private void TextBox_OnTextChanged(object sender, TextChangedEventArgs e)
{
if (DataContext is not CompatibilityViewModel cvm)
return;

View File

@@ -11,14 +11,13 @@ namespace Ryujinx.Ava.Utilities.Compat
{
[ObservableProperty] private bool _onlyShowOwnedGames = true;
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityCsv.Shared.Entries;
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityCsv.Entries;
private readonly string[] _ownedGameTitleIds = [];
private readonly ApplicationLibrary _appLibrary;
public IEnumerable<CompatibilityEntry> CurrentEntries => OnlyShowOwnedGames
? _currentEntries.Where(x =>
x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid))
|| _appLibrary.Applications.Items.Any(a => a.Name.EqualsIgnoreCase(x.GameName)))
x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid)))
: _currentEntries;
public CompatibilityViewModel() {}
@@ -39,11 +38,11 @@ namespace Ryujinx.Ava.Utilities.Compat
{
if (string.IsNullOrEmpty(searchTerm))
{
SetEntries(CompatibilityCsv.Shared.Entries);
SetEntries(CompatibilityCsv.Entries);
return;
}
SetEntries(CompatibilityCsv.Shared.Entries.Where(x =>
SetEntries(CompatibilityCsv.Entries.Where(x =>
x.GameName.ContainsIgnoreCase(searchTerm)
|| x.TitleId.Check(tid => tid.ContainsIgnoreCase(searchTerm))));
}

View File

@@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 59;
public const int CurrentVersion = 60;
/// <summary>
/// Version of the configuration file format
@@ -366,6 +366,12 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// </summary>
public bool EnableMouse { get; set; }
/// <summary>
/// Allows you to choose one of several behaviors when pressing hotkeys:
/// 0 - Do nothing, 1 - Close the emulator application, 2 - Exit the game.
/// </summary>
public int SpecialExitEmulator { get; set; }
/// <summary>
/// Hotkey Keyboard Bindings
/// </summary>

View File

@@ -136,6 +136,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
Hid.EnableKeyboard.Value = cff.EnableKeyboard;
Hid.EnableMouse.Value = cff.EnableMouse;
Hid.SpecialExitEmulator.Value = cff.SpecialExitEmulator;
Hid.Hotkeys.Value = cff.Hotkeys;
Hid.InputConfig.Value = cff.InputConfig ?? [];
@@ -414,6 +415,10 @@ namespace Ryujinx.Ava.Utilities.Configuration
// This was accidentally enabled by default when it was PRed. That is not what we want,
// so as a compromise users who want to use it will simply need to re-enable it once after updating.
cff.IgnoreApplet = false;
}),
(60, static cff =>
{
cff.SpecialExitEmulator = 0;
})
);
}

View File

@@ -420,6 +420,13 @@ namespace Ryujinx.Ava.Utilities.Configuration
/// </summary>
public ReactiveObject<bool> EnableMouse { get; private set; }
/// <summary>
/// Allows you to choose one of several behaviors when pressing hotkeys:
/// 0 - Do nothing, 1 - Close the emulator application, 2 - Exit the game.
/// </summary>
public ReactiveObject<int> SpecialExitEmulator { get; private set; }
/// <summary>
/// Hotkey Keyboard Bindings
/// </summary>
@@ -436,6 +443,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
{
EnableKeyboard = new ReactiveObject<bool>();
EnableMouse = new ReactiveObject<bool>();
SpecialExitEmulator = new ReactiveObject<int>();
Hotkeys = new ReactiveObject<KeyboardHotkeys>();
InputConfig = new ReactiveObject<List<InputConfig>>();
}

View File

@@ -128,6 +128,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
ShowConsole = UI.ShowConsole,
EnableKeyboard = Hid.EnableKeyboard,
EnableMouse = Hid.EnableMouse,
SpecialExitEmulator = Hid.SpecialExitEmulator,
Hotkeys = Hid.Hotkeys,
KeyboardConfig = [],
ControllerConfig = [],
@@ -241,6 +242,7 @@ namespace Ryujinx.Ava.Utilities.Configuration
UI.WindowStartup.WindowMaximized.Value = false;
Hid.EnableKeyboard.Value = false;
Hid.EnableMouse.Value = false;
Hid.SpecialExitEmulator.Value = 0;
Hid.Hotkeys.Value = new KeyboardHotkeys
{
ToggleVSyncMode = Key.F1,

View File

@@ -1,3 +1,5 @@
using Humanizer;
using Humanizer.Localisation;
using Ryujinx.Ava.Common.Locale;
using System;
using System.Globalization;
@@ -31,7 +33,7 @@ namespace Ryujinx.Ava.Utilities
Gigabytes = 9,
Terabytes = 10,
Petabytes = 11,
Exabytes = 12,
Exabytes = 12
}
private const double SizeBase10 = 1000;
@@ -48,22 +50,24 @@ namespace Ryujinx.Ava.Utilities
public static string FormatTimeSpan(TimeSpan? timeSpan)
{
if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
{
// Game was never played
return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
}
return string.Empty;
if (timeSpan.Value.TotalSeconds < 60)
return timeSpan.Value.Humanize(1,
countEmptyUnits: false,
maxUnit: TimeUnit.Second,
minUnit: TimeUnit.Second);
if (timeSpan.Value.TotalDays < 1)
{
// Game was played for less than a day
return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
}
// Game was played for more than a day
TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
return $"{timeSpan.Value.Days}d, {onlyTimeString}";
if (timeSpan.Value.TotalMinutes < 60)
return timeSpan.Value.Humanize(1,
countEmptyUnits: false,
maxUnit: TimeUnit.Minute,
minUnit: TimeUnit.Minute);
return timeSpan.Value.Humanize(1,
countEmptyUnits: false,
maxUnit: TimeUnit.Hour,
minUnit: TimeUnit.Hour);
}
/// <summary>