Compare commits

...

63 Commits

Author SHA1 Message Date
ed64a63094 UI: Visually merge "Actions" and "Tools" menu bar items into Actions
The contents of the menu item are dependent on whether you're in a game.
No functionality has been removed.
2025-01-20 16:56:05 -06:00
8df7ba2d56 i18n: Norwegian DLC RomFS dumping translation 2025-01-20 15:55:37 -06:00
e743d78115 Add/fix service reported info (#551)
fixes the GetConfig service call, which now returns success correctly
adds support for getting the device serial number (which is fake and
reports as "RYU00000000000")
2025-01-20 14:59:54 -06:00
04ba762710 UI: Move DLC RomFS dumping under normal RomFS dumping.
Also removed it from DLC manager.
2025-01-20 14:30:28 -06:00
f42b2ed59d misc: chore: more correct last used user checking 2025-01-20 13:33:59 -06:00
d135385cab Leftovers, oops 2025-01-20 09:32:38 -06:00
b360f4e721 UI: Dump DLC RomFS.
You can access this in the Manage DLC screen, it's the new button on each DLC line.

Closes #548
2025-01-20 09:28:58 -06:00
a1c0c70ec2 Added missing TitleIDs (#545)
Added the remaining missing TitleIDs in the compat.csv list.
Excluding Homebrew apps.
2025-01-19 21:15:46 -06:00
290ac405ac Updated Ukrainian translation by Rondo (#543)
Co-authored-by: rrondo <46533574+rrondo@users.noreply.github.com>
2025-01-19 21:00:40 -06:00
bbd64fd5f0 misc: chore: Cleanup AppletMetadata usage 2025-01-19 19:40:49 -06:00
09446fd80e [ci skip] infra: Update copyright year in shortcut plist 2025-01-19 18:57:05 -06:00
4f014a89cf docs: compat: Donkey Kong Country Returns HD 2025-01-19 18:28:43 -06:00
6482e566ab UI: Compat: Unload compatibility entries when the window closes. 2025-01-19 17:41:50 -06:00
7fcd9b792e UI: Compat: Update owned game title IDs when ApplicationLibrary app count updates 2025-01-19 17:41:31 -06:00
e676fd8b17 UI: misc: simplify Intel Mac warning logic 2025-01-19 14:42:15 -06:00
dd16e3cee1 misc: chore: very small cleanup in AvaHostUIHandler 2025-01-19 13:18:40 -06:00
31e5f74e05 UI: misc: Replace spaces in Title with newlines when using custom title bar (since the Title is in an Avalonia tooltip) 2025-01-19 13:05:20 -06:00
f2f099bddb remove Async suffixes; they're factory methods not actual async methods. 2025-01-19 12:46:32 -06:00
2616dc57fb misc: chore: RelayCommand helper 2025-01-19 12:44:07 -06:00
0cdf7cfe21 UI: Open cheat manager in catch-all try 2025-01-18 22:48:06 -06:00
2ecf999569 misc: chore: change ThemeManager ThemeChanged to a basic Action since both arguments are unused 2025-01-18 22:48:06 -06:00
b612fc5155 Updated TitleIDs (#541)
Added more games to the RPC list. Now alphabetical.
2025-01-18 22:19:28 -06:00
25eb545409 Update bug_report.yml 2025-01-18 20:55:28 -06:00
52269964b6 Add the player select applet. (#537)
This introduces the somewhat completed version of the Player Select
Applet, allowing users to select either a user or a guest from the UI.
Note: Selecting the guest more then once currently does not work.

closes https://github.com/Ryubing/Ryujinx/issues/532
2025-01-18 20:40:33 -06:00
ccdddac8fc Fix compile warnings 2025-01-18 19:34:31 -06:00
1bc30bf3ba Update locales.json (#538)
Added a missing translation line
2025-01-18 18:40:51 -06:00
4868fface8 UI: Intel Mac warning
Upon launch, shows a warning about using an Intel Mac. This will only show once every boot. You can only turn it off by getting a better system.
2025-01-18 15:33:05 -06:00
6fca4492d0 misc: chore: Remove status update event stuff in Headless 2025-01-18 15:15:08 -06:00
ade2f256e0 misc: chore: remove duplicate graphics debug levels in headless windows 2025-01-18 11:19:38 -06:00
580b150c9a Revert "infra: Conditionally compile Metal & OpenGL depending on if the target RuntimeIdentifier is mac"
This reverts commit 2f93a0f706.
2025-01-18 10:57:02 -06:00
e6bad52945 Revert "Only selectively compile Metal & fix some compilation issues"
This reverts commit beda3206e0.
2025-01-18 10:56:58 -06:00
beda3206e0 Only selectively compile Metal & fix some compilation issues 2025-01-18 10:52:32 -06:00
2f93a0f706 infra: Conditionally compile Metal & OpenGL depending on if the target RuntimeIdentifier is mac 2025-01-18 10:38:29 -06:00
80f44d9547 misc: chore: small cleanup 2025-01-18 10:33:57 -06:00
b08e5db6d8 Headless: Dispose of inputmanager in a catch-all try 2025-01-18 10:30:19 -06:00
6a291d4116 Headless: Use main UI logo for window icon instead of separate bmp 2025-01-18 10:26:12 -06:00
6fc827fe67 headless: collapse headless window definition into a "Windows" folder, change GetWindowFlags to an abstract property. 2025-01-18 10:15:24 -06:00
6cd4866d76 Updated sv_SE in locales.json (#513) 2025-01-17 18:04:18 -06:00
4d7ca5c0f0 Update Chinese translations (#375) 2025-01-17 17:35:34 -06:00
a375faecc1 UI: Fix UpdateWaitWindow.axaml windows being too big on windows (#314) 2025-01-17 14:14:19 -06:00
1728b0f20c WWE 2K18 is not playable. 2025-01-17 11:37:08 -06:00
5aa071c59b remove notice for unusual core counts (#531) 2025-01-17 05:50:42 -06:00
1018c9db8b Update Norwegian Translation (#503)
Norwegian translation updated with the Compatibility list addition
2025-01-16 10:02:33 -06:00
01ccd18726 UI: Meant to use that method in another place [ci-skip] 2025-01-16 09:52:35 -06:00
abfbc6f4bc UI: Prevent desynced RPC when toggling it off/on while in-game 2025-01-16 09:52:01 -06:00
6a4bc02d7a Update Italian translation (#489) 2025-01-16 06:38:36 -06:00
814c0526d2 Korean translations for compat list (#502) 2025-01-16 04:57:32 -06:00
a5a4ef38e6 HLE: Stub IHidServer SetGestureOutputRanges (#524)
Lets "Donkey Kong Country Returns HD" get into main gameplay.
2025-01-16 02:39:39 -06:00
c17e3bfcdf genuinely dont know how that was still there, i thought i got rid of UI.Common 2025-01-15 03:01:17 -06:00
017f46f318 HLE: misc: throw a more descriptive error when the loaded processes doesn't contain _latestPid (likely missing FW) 2025-01-15 03:01:17 -06:00
fd4d801bfd Various NuGet package updates (#203)
Updates the following packages:

**nuget: bump the avalonia group with 7 updates**

* Bump Avalonia, Avalonia.Controls.DataGrid, Avalonia.Desktop,
Avalonia.Diagnostics, and Avalonia.Markup.Xaml.Loader from 11.0.10 to
11.0.13
* Bump Avalonia.Svg and Avalonia.Svg.Skia from 11.0.0.18 to 11.0.0.19

**nuget: bump non-avalonia packages**

* Bump Concentus from 2.2.0 to 2.2.2
* Bump Microsoft.IdentityModel.JsonWebTokens from 8.1.2 to 8.3.0
* Bump Silk.NET.Vulkan group with 3 updates (2.21.0 to 2.22.0)
* Bump SkiaSharp group with 2 updates (2.88.7 to 2.88.9)
2025-01-13 11:15:05 -06:00
f1dee50275 infra: Update to LLVM 17 (#519)
This fixes macos builds not building correctly because of a missing
LLVM 14 package.
2025-01-12 14:57:57 -06:00
c2ae49eb47 Add some missing Simplified Chinese translations (#515) 2025-01-12 12:33:27 -06:00
47c71966d0 Add compat of Xenoblade 2 JP edition same as global edition (#517) 2025-01-12 12:31:58 -06:00
f9e8f4bc29 docs: compat: Ori and the Will of the Wisps is now ingame, not playable 2025-01-11 06:10:58 -06:00
7694c8c046 Do not auto release stable 2025-01-11 04:08:11 -06:00
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
75 changed files with 5110 additions and 4482 deletions

View File

@ -22,7 +22,7 @@ body:
id: log
attributes:
label: Log file
description: A log file will help our developers to better diagnose and fix the issue.
description: "A log file will help our developers to better diagnose and fix the issue. UPLOAD THE FILE. DO NOT COPY AND PASTE THE FILE'S CONTENT."
placeholder: Logs files can be found under "Logs" folder in Ryujinx program folder. They can also be accessed by opening Ryujinx, then going to File > Open Logs Folder. You can drag and drop the log on to the text area (do not copy paste).
validations:
required: true

View File

@ -129,11 +129,11 @@ jobs:
with:
global-json-file: global.json
- name: Setup LLVM 14
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 14
sudo ./llvm.sh 17
- name: Install rcodesign
run: |

View File

@ -210,11 +210,11 @@ jobs:
with:
global-json-file: global.json
- name: Setup LLVM 15
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 15
sudo ./llvm.sh 17
- name: Install rcodesign
run: |

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
@ -201,11 +191,11 @@ jobs:
with:
global-json-file: global.json
- name: Setup LLVM 15
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 15
sudo ./llvm.sh 17
- name: Install rcodesign
run: |

View File

@ -3,13 +3,13 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.0.10" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.10" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.10" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.18" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.18" />
<PackageVersion Include="Avalonia" Version="11.0.13" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.13" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.13" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.13" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.19" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.19" />
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
@ -18,7 +18,7 @@
<PackageVersion Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.4.0"/>
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0"/>
<PackageVersion Include="Concentus" Version="2.2.0" />
<PackageVersion Include="Concentus" Version="2.2.2" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="DynamicData" Version="9.0.4" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
@ -26,7 +26,7 @@
<PackageVersion Include="LibHac" Version="0.19.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.1.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
@ -48,11 +48,11 @@
<PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="SharpMetal" Version="1.0.0-preview21" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.21.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.21.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.21.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.7" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.22.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.22.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.22.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageVersion Include="SPB" Version="0.0.4-build32" />
<PackageVersion Include="System.IO.Hashing" Version="9.0.0" />
<PackageVersion Include="System.Management" Version="9.0.0" />

View File

@ -19,7 +19,7 @@ if platform.system() == "Darwin":
else:
OTOOL = shutil.which("llvm-otool")
if OTOOL is None:
for llvm_ver in [15, 14, 13]:
for llvm_ver in [17, 16, 15, 14, 13]:
otool_path = shutil.which(f"llvm-otool-{llvm_ver}")
if otool_path is not None:
OTOOL = otool_path

View File

@ -26,7 +26,7 @@ else:
LIPO = shutil.which("llvm-lipo")
if LIPO is None:
for llvm_ver in [15, 14, 13]:
for llvm_ver in [17, 16, 15, 14, 13]:
lipo_path = shutil.which(f"llvm-lipo-{llvm_ver}")
if lipo_path is not None:
LIPO = lipo_path

View File

@ -67,11 +67,11 @@ python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_
if ! [ -x "$(command -v lipo)" ];
then
if ! [ -x "$(command -v llvm-lipo-14)" ];
if ! [ -x "$(command -v llvm-lipo-17)" ];
then
LIPO=llvm-lipo
else
LIPO=llvm-lipo-14
LIPO=llvm-lipo-17
fi
else
LIPO=lipo

View File

@ -62,11 +62,11 @@ python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_OUTP
if ! [ -x "$(command -v lipo)" ];
then
if ! [ -x "$(command -v llvm-lipo-14)" ];
if ! [ -x "$(command -v llvm-lipo-17)" ];
then
LIPO=llvm-lipo
else
LIPO=llvm-lipo-14
LIPO=llvm-lipo-17
fi
else
LIPO=lipo

View File

@ -19,7 +19,7 @@
<key>CSResourcesFileMapped</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018 - 2023 Ryujinx Team and Contributors.</string>
<string>Copyright © 2018 - 2025 Ryujinx Team and Contributors.</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>LSMinimumSystemVersion</key>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
using System;
namespace Ryujinx.Common
{
public class RyujinxException : Exception
{
public RyujinxException(string message) : base(message)
{ }
}
}

View File

@ -1,4 +1,4 @@
using Gommon;
using Gommon;
using Ryujinx.Common.Configuration;
using System;
using System.Linq;
@ -47,6 +47,9 @@ namespace Ryujinx.Common
public static readonly string[] DiscordGameAssetKeys =
[
"010008900705c000", // Dragon Quest Builders
"010042000a986000", // Dragon Quest Builders 2
"010055d009f78000", // Fire Emblem: Three Houses
"0100a12011cc8000", // Fire Emblem: Shadow Dragon
"0100a6301214e000", // Fire Emblem Engage
@ -105,17 +108,18 @@ namespace Ryujinx.Common
"0100f4c009322000", // Pikmin 3 Deluxe
"0100b7c00933a000", // Pikmin 4
"0100f4300bf2c000", // New Pokémon Snap
"0100000011d90000", // Pokémon Brilliant Diamond
"01001f5010dfa000", // Pokémon Legends: Arceus
"010003f003a34000", // Pokémon: Let's Go Pikachu!
"0100187003a36000", // Pokémon: Let's Go Eevee!
"0100abf008968000", // Pokémon Sword
"01008db008c2c000", // Pokémon Shield
"0100000011d90000", // Pokémon Brilliant Diamond
"010018e011d92000", // Pokémon Shining Pearl
"01001f5010dfa000", // Pokémon Legends: Arceus
"01003d200baa2000", // Pokémon Mystery Dungeon - Rescue Team DX
"0100a3d008c5c000", // Pokémon Scarlet
"01008db008c2c000", // Pokémon Shield
"010018e011d92000", // Pokémon Shining Pearl
"0100abf008968000", // Pokémon Sword
"01008f6008c5e000", // Pokémon Violet
"0100b3f000be2000", // Pokkén Tournament DX
"0100f4300bf2c000", // New Pokémon Snap
"01003bc0000a0000", // Splatoon 2 (US)
"0100f8f0000a2000", // Splatoon 2 (EU)
@ -165,13 +169,21 @@ namespace Ryujinx.Common
"01005ea01c0fc000", // SONIC X SHADOW GENERATIONS
"01005ea01c0fc001", // ^
"0100ff500e34a000", // Xenoblade Chronicles - Definitive Edition
"0100e95004038000", // Xenoblade Chronicles 2
"010074f013262000", // Xenoblade Chronicles 3
"010056e00853a000", // A Hat in Time
"0100fd1014726000", // Baldurs Gate: Dark Alliance
"0100dbf01000a000", // Burnout Paradise Remastered
"0100744001588000", // Cars 3: Driven to Win
"0100b41013c82000", // Cruis'n Blast
"010085900337e000", // Death Squared
"01001b300b9be000", // Diablo III: Eternal Collection
"01008c8012920000", // Dying Light Platinum Edition
"01001cc01b2d4000", // Goat Simulator 3
"01003620068ea000", // Hand of Fate 2
"010085500130a000", // Lego City: Undercover
"010073c01af34000", // LEGO Horizon Adventures
"0100770008dd8000", // Monster Hunter Generations Ultimate
"0100b04011742000", // Monster Hunter Rise
@ -190,6 +202,8 @@ namespace Ryujinx.Common
"01000a10041ea000", // The Elder Scrolls V: Skyrim
"010057a01e4d4000", // TSUKIHIME -A piece of blue glass moon-
"010080b00ad66000", // Undertale
"010069401adb8000", // Unicorn Overlord
"0100534009ff2000", // Yonder - The cloud catcher chronicles
];
}
}

View File

@ -92,7 +92,7 @@ namespace Ryujinx.Graphics.Vulkan
DriverId.MesaDozen => "Dozen",
DriverId.MesaNvk => "NVK",
DriverId.ImaginationOpenSourceMesa => "Imagination (Open)",
DriverId.MesaAgxv => "Honeykrisp",
DriverId.MesaHoneykrisp => "Honeykrisp",
_ => id.ToString(),
};
}

View File

@ -26,10 +26,20 @@ namespace Ryujinx.HLE.HOS.Applets
{
_normalSession = normalSession;
_interactiveSession = interactiveSession;
// TODO(jduncanator): Parse PlayerSelectConfig from input data
_normalSession.Push(BuildResponse());
UserProfile selected = _system.Device.UIHandler.ShowPlayerSelectDialog();
if (selected == null)
{
_normalSession.Push(BuildResponse());
}
else if (selected.UserId == new UserId("00000000000000000000000000000080"))
{
_normalSession.Push(BuildGuestResponse());
}
else
{
_normalSession.Push(BuildResponse(selected));
}
AppletStateChanged?.Invoke(this, null);
_system.ReturnFocus();
@ -37,16 +47,34 @@ namespace Ryujinx.HLE.HOS.Applets
return ResultCode.Success;
}
private byte[] BuildResponse()
private byte[] BuildResponse(UserProfile selectedUser)
{
UserProfile currentUser = _system.AccountManager.LastOpenedUser;
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream);
writer.Write((ulong)PlayerSelectResult.Success);
currentUser.UserId.Write(writer);
selectedUser.UserId.Write(writer);
return stream.ToArray();
}
private byte[] BuildGuestResponse()
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream);
writer.Write(new byte());
return stream.ToArray();
}
private byte[] BuildResponse()
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
using BinaryWriter writer = new(stream);
writer.Write((ulong)PlayerSelectResult.Failure);
return stream.ToArray();
}

View File

@ -176,9 +176,7 @@ 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)
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

@ -56,7 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, _serializerContext.ProfilesJson);
return profilesJson.Profiles
.FindFirst(profile => profile.AccountState == AccountState.Open)
.FindFirst(profile => profile.UserId == profilesJson.LastOpened)
.Convert(profileJson => new UserProfile(new UserId(profileJson.UserId), profileJson.Name,
profileJson.Image, profileJson.LastModifiedTimestamp));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -702,6 +702,18 @@ namespace Ryujinx.HLE.HOS.Services.Hid
return ResultCode.Success;
}
[CommandCmif(92)]
// SetGestureOutputRanges(pid, ushort Unknown0)
public ResultCode SetGestureOutputRanges(ServiceCtx context)
{
ulong pid = context.Request.HandleDesc.PId;
ushort unknown0 = context.RequestData.ReadUInt16();
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { pid, unknown0 });
return ResultCode.Success;
}
[CommandCmif(100)]
// SetSupportedNpadStyleSet(pid, nn::applet::AppletResourceUserId, nn::hid::NpadStyleTag)

View File

@ -244,6 +244,15 @@ namespace Ryujinx.HLE.HOS.Services.Settings
return ResultCode.Success;
}
[CommandCmif(68)]
// GetSerialNumber() -> buffer<nn::settings::system::SerialNumber, 0x16>
public ResultCode GetSerialNumber(ServiceCtx context)
{
context.ResponseData.Write(Encoding.ASCII.GetBytes("RYU00000000000"));
return ResultCode.Success;
}
[CommandCmif(77)]
// GetDeviceNickName() -> buffer<nn::settings::system::DeviceNickName, 0x16>
public ResultCode GetDeviceNickName(ServiceCtx context)

View File

@ -51,6 +51,9 @@ namespace Ryujinx.HLE.HOS.Services.Spl
context.ResponseData.Write(configValue);
if (result == SmcResult.Success)
return ResultCode.Success;
return (ResultCode)((int)result << 9) | ResultCode.ModuleId;
}

View File

@ -26,7 +26,17 @@ namespace Ryujinx.HLE.Loaders.Processes
private ulong _latestPid;
public ProcessResult ActiveApplication => _processesByPid[_latestPid];
public ProcessResult ActiveApplication
{
get
{
if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value))
throw new RyujinxException(
$"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?");
return value;
}
}
public ProcessLoader(Switch device)
{

View File

@ -48,6 +48,7 @@
<EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_BtnB.png" />
<EmbeddedResource Include="HOS\Applets\SoftwareKeyboard\Resources\Icon_KeyF6.png" />
<EmbeddedResource Include="HOS\Services\Account\Acc\DefaultUserImage.jpg" />
<EmbeddedResource Include="HOS\Services\Account\Acc\GuestUserImage.jpg" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
namespace Ryujinx.HLE.UI
@ -59,5 +60,11 @@ namespace Ryujinx.HLE.UI
/// Gets fonts and colors used by the host.
/// </summary>
IHostUITheme HostUITheme { get; }
/// <summary>
/// Displays the player select dialog and returns the selected profile.
/// </summary>
UserProfile ShowPlayerSelectDialog();
}
}

View File

@ -1,36 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\Controller_JoyConLeft.svg" />
<None Remove="Resources\Controller_JoyConPair.svg" />
<None Remove="Resources\Controller_JoyConRight.svg" />
<None Remove="Resources\Controller_ProCon.svg" />
<None Remove="Resources\Icon_NCA.png" />
<None Remove="Resources\Icon_NRO.png" />
<None Remove="Resources\Icon_NSO.png" />
<None Remove="Resources\Icon_NSP.png" />
<None Remove="Resources\Icon_XCI.png" />
<None Remove="Resources\Logo_Amiibo.png" />
<None Remove="Resources\Logo_Discord.png" />
<None Remove="Resources\Logo_GitHub.png" />
<None Remove="Resources\Logo_Ryujinx.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscordRichPresence" />
<PackageReference Include="DynamicData" />
<PackageReference Include="securifybv.ShellLink" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
</ItemGroup>
</Project>

View File

@ -489,7 +489,7 @@ namespace Ryujinx.Ava
Dispatcher.UIThread.InvokeAsync(() =>
{
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version);
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowTitleBar);
});
_viewModel.SetUiProgressHandlers(Device);
@ -872,7 +872,7 @@ namespace Ryujinx.Ava
Device?.System.TogglePauseEmulation(false);
_viewModel.IsPaused = false;
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version);
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowTitleBar);
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed");
}
@ -881,7 +881,7 @@ namespace Ryujinx.Ava
Device?.System.TogglePauseEmulation(true);
_viewModel.IsPaused = true;
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, LocaleManager.Instance[LocaleKeys.Paused]);
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowTitleBar, LocaleManager.Instance[LocaleKeys.Paused]);
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
}

File diff suppressed because it is too large Load Diff

View File

@ -295,6 +295,142 @@ namespace Ryujinx.Ava.Common
};
extractorThread.Start();
}
public static void ExtractAoc(string destination, string updateFilePath, string updateName)
{
var cancellationToken = new CancellationTokenSource();
UpdateWaitWindow waitingDialog = new(
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, NcaSectionType.Data, Path.GetFileName(updateFilePath)),
cancellationToken);
Thread extractorThread = new(() =>
{
Dispatcher.UIThread.Post(waitingDialog.Show);
using FileStream file = new(updateFilePath, FileMode.Open, FileAccess.Read);
Nca publicDataNca = null;
string extension = Path.GetExtension(updateFilePath).ToLower();
if (extension is ".nsp")
{
var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
IFileSystem pfs = pfsTemp;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
if (nca.Header.ContentType is NcaContentType.PublicData && nca.SectionExists(NcaSectionType.Data))
{
publicDataNca = nca;
}
}
}
if (publicDataNca is null)
{
Logger.Error?.Print(LogClass.Application, "Extraction failure. The NCA was not present in the selected file");
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]);
});
return;
}
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
int index = Nca.GetSectionIndexFromType(NcaSectionType.Data, publicDataNca.Header.ContentType);
try
{
IFileSystem ncaFileSystem = publicDataNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid);
FileSystemClient fsClient = _horizonClient.Fs;
string source = DateTime.Now.ToFileTime().ToString()[10..];
string output = DateTime.Now.ToFileTime().ToString()[10..];
using var uniqueSourceFs = new UniqueRef<IFileSystem>(ncaFileSystem);
using var uniqueOutputFs = new UniqueRef<IFileSystem>(new LocalFileSystem(destination));
fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref);
fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref);
(Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/", cancellationToken.Token);
if (!canceled)
{
if (resultCode.Value.IsFailure())
{
Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}");
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]);
});
}
else if (resultCode.Value.IsSuccess())
{
Dispatcher.UIThread.Post(waitingDialog.Close);
NotificationHelper.ShowInformation(
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
$"{updateName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
}
}
fsClient.Unmount(source.ToU8Span());
fsClient.Unmount(output.ToU8Span());
}
catch (ArgumentException ex)
{
Logger.Error?.Print(LogClass.Application, $"{ex.Message}");
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
await ContentDialogHelper.CreateErrorDialog(ex.Message);
});
}
})
{
Name = "GUI.AocExtractorThread",
IsBackground = true,
};
extractorThread.Start();
}
public static async Task ExtractAoc(IStorageProvider storageProvider, string updateFilePath, string updateName)
{
var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle],
AllowMultiple = false,
});
if (result.Count == 0)
{
return;
}
ExtractAoc(result[0].Path.LocalPath, updateFilePath, updateName);
}
public static async Task ExtractSection(IStorageProvider storageProvider, NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0)

View File

@ -6,7 +6,7 @@ namespace Ryujinx.Ava.Common.Models
public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci";
public string FileName => System.IO.Path.GetFileName(ContainerPath);
public string TitleIdStr => TitleId.ToString("x16");
public string TitleIdStr => TitleId.ToString("x16").ToUpper();
public ulong TitleIdBase => TitleId & ~0x1FFFUL;
}
}

View File

@ -4,11 +4,11 @@ namespace Ryujinx.Ava.Common
{
public static class ThemeManager
{
public static event EventHandler ThemeChanged;
public static event Action ThemeChanged;
public static void OnThemeChanged()
{
ThemeChanged?.Invoke(null, EventArgs.Empty);
ThemeChanged?.Invoke();
}
}
}

View File

@ -1,6 +1,8 @@
using DiscordRPC;
using Gommon;
using Humanizer;
using Humanizer.Localisation;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common;
@ -45,16 +47,7 @@ namespace Ryujinx.Ava
};
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
TitleIDs.CurrentApplication.Event += (_, e) =>
{
if (e.NewValue)
SwitchToPlayingState(
ApplicationLibrary.LoadAndSaveMetaData(e.NewValue),
Switch.Shared.Processes.ActiveApplication
);
else
SwitchToMainState();
};
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
}
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
@ -75,11 +68,23 @@ namespace Ryujinx.Ava
_discordClient = new DiscordRpcClient(ApplicationId);
_discordClient.Initialize();
_discordClient.SetPresence(_discordPresenceMain);
Use(TitleIDs.CurrentApplication);
}
}
}
public static void Use(Optional<string> titleId)
{
if (titleId.TryGet(out string tid))
SwitchToPlayingState(
ApplicationLibrary.LoadAndSaveMetaData(tid),
Switch.Shared.Processes.ActiveApplication
);
else
SwitchToMainState();
}
private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
{
_discordClient?.SetPresence(new RichPresence
@ -93,7 +98,7 @@ namespace Ryujinx.Ava
},
Details = TruncateToByteLength($"Playing {appMeta.Title}"),
State = appMeta.LastPlayed.HasValue && appMeta.TimePlayed.TotalSeconds > 5
? $"Total play time: {appMeta.TimePlayed.Humanize(2, false, maxUnit: TimeUnit.Hour)}"
? $"Total play time: {ValueFormatUtils.FormatTimeSpan(appMeta.TimePlayed)}"
: "Never played",
Timestamps = Timestamps.Now
});

View File

@ -228,8 +228,6 @@ namespace Ryujinx.Headless
_inputConfiguration ??= [];
_enableKeyboard = option.EnableKeyboard;
_enableMouse = option.EnableMouse;
LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1);
LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2);
@ -301,7 +299,10 @@ namespace Ryujinx.Headless
_userChannelPersistence.ShouldRestart = false;
}
_inputManager.Dispose();
try
{
_inputManager.Dispose();
} catch {}
return;
@ -338,12 +339,12 @@ namespace Ryujinx.Headless
{
string label = state switch
{
LoadState => $"PTC : {current}/{total}",
ShaderCacheState => $"Shaders : {current}/{total}",
_ => throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"),
LoadState => "PTC",
ShaderCacheState => "Shaders",
_ => throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}")
};
Logger.Info?.Print(LogClass.Application, label);
Logger.Info?.Print(LogClass.Application, $"{label} : {current}/{total}");
}
private static WindowBase CreateWindow(Options options)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,21 +0,0 @@
using System;
namespace Ryujinx.Headless
{
class StatusUpdatedEventArgs(
string vSyncMode,
string dockedMode,
string aspectRatio,
string gameStatus,
string fifoStatus,
string gpuName)
: EventArgs
{
public string VSyncMode = vSyncMode;
public string DockedMode = dockedMode;
public string AspectRatio = aspectRatio;
public string GameStatus = gameStatus;
public string FifoStatus = fifoStatus;
public string GpuName = gpuName;
}
}

View File

@ -26,7 +26,7 @@ namespace Ryujinx.Headless
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { }
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_METAL;
public override SDL_WindowFlags WindowFlags => SDL_WindowFlags.SDL_WINDOW_METAL;
protected override void InitializeWindowRenderer()
{

View File

@ -108,8 +108,7 @@ namespace Ryujinx.Headless
}
}
}
private readonly GraphicsDebugLevel _glLogLevel;
private SDL2OpenGLContext _openGLContext;
public OpenGLWindow(
@ -121,15 +120,14 @@ namespace Ryujinx.Headless
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
{
_glLogLevel = glLogLevel;
}
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_OPENGL;
public override SDL_WindowFlags WindowFlags => SDL_WindowFlags.SDL_WINDOW_OPENGL;
protected override void InitializeWindowRenderer()
{
// Ensure to not share this context with other contexts before this point.
SetupOpenGLAttributes(false, _glLogLevel);
SetupOpenGLAttributes(false, GlLogLevel);
nint context = SDL_GL_CreateContext(WindowHandle);
CheckResult(SDL_GL_SetSwapInterval(1));

View File

@ -10,8 +10,6 @@ namespace Ryujinx.Headless
{
class VulkanWindow : WindowBase
{
private readonly GraphicsDebugLevel _glLogLevel;
public VulkanWindow(
InputManager inputManager,
GraphicsDebugLevel glLogLevel,
@ -21,10 +19,9 @@ namespace Ryujinx.Headless
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
{
_glLogLevel = glLogLevel;
}
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_VULKAN;
public override SDL_WindowFlags WindowFlags => SDL_WindowFlags.SDL_WINDOW_VULKAN;
protected override void InitializeWindowRenderer() { }

View File

@ -1,14 +1,15 @@
using Humanizer;
using LibHac.Tools.Fs;
using Ryujinx.Ava;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.HLE.UI;
using Ryujinx.Input;
@ -26,6 +27,7 @@ using static SDL2.SDL;
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
using Switch = Ryujinx.HLE.Switch;
using UserProfile = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
namespace Ryujinx.Headless
{
@ -53,8 +55,6 @@ namespace Ryujinx.Headless
public Switch Device { get; private set; }
public IRenderer Renderer { get; private set; }
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
protected nint WindowHandle { get; set; }
public IHostUITheme HostUITheme { get; }
@ -72,7 +72,7 @@ namespace Ryujinx.Headless
protected SDL2MouseDriver MouseDriver;
private readonly InputManager _inputManager;
private readonly IKeyboard _keyboardInterface;
private readonly GraphicsDebugLevel _glLogLevel;
protected readonly GraphicsDebugLevel GlLogLevel;
private readonly Stopwatch _chrono;
private readonly long _ticksPerFrame;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
@ -104,7 +104,7 @@ namespace Ryujinx.Headless
NpadManager = _inputManager.CreateNpadManager();
TouchScreenManager = _inputManager.CreateTouchScreenManager();
_keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
_glLogLevel = glLogLevel;
GlLogLevel = glLogLevel;
_chrono = new Stopwatch();
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_gpuCancellationTokenSource = new CancellationTokenSource();
@ -137,7 +137,7 @@ namespace Ryujinx.Headless
private void SetWindowIcon()
{
Stream iconStream = typeof(Program).Assembly.GetManifestResourceStream("HeadlessLogo");
Stream iconStream = EmbeddedResources.GetStream("Ryujinx/Assets/UIImages/Logo_Ryujinx.png");
byte[] iconBytes = new byte[iconStream!.Length];
if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
@ -191,7 +191,7 @@ namespace Ryujinx.Headless
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
}
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | WindowFlags);
if (WindowHandle == nint.Zero)
{
@ -246,7 +246,7 @@ namespace Ryujinx.Headless
protected abstract void SwapBuffers();
public abstract SDL_WindowFlags GetWindowFlags();
public abstract SDL_WindowFlags WindowFlags { get; }
private string GetGpuDriverName()
{
@ -268,7 +268,7 @@ namespace Ryujinx.Headless
{
InitializeWindowRenderer();
Device.Gpu.Renderer.Initialize(_glLogLevel);
Device.Gpu.Renderer.Initialize(GlLogLevel);
InitializeRenderer();
@ -308,21 +308,6 @@ namespace Ryujinx.Headless
if (_ticks >= _ticksPerFrame)
{
string dockedMode = Device.System.State.DockedMode ? "Docked" : "Handheld";
float scale = GraphicsConfig.ResScale;
if (scale != 1)
{
dockedMode += $" ({scale}x)";
}
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.VSyncMode.ToString(),
dockedMode,
Device.Configuration.AspectRatio.ToText(),
$"{Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
$"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
$"GPU: {_gpuDriverName}"));
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
}
}
@ -572,5 +557,10 @@ namespace Ryujinx.Headless
SDL2Driver.Instance.Dispose();
}
}
public UserProfile ShowPlayerSelectDialog()
{
return AccountSaveDataManager.GetLastUsedUser();
}
}
}

View File

@ -21,6 +21,7 @@ using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless;
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@ -243,16 +244,33 @@ namespace Ryujinx.Ava
: $"Launch Mode: {AppDataManager.Mode}");
}
internal static void ProcessUnhandledException(object sender, Exception ex, bool isTerminating)
internal static void ProcessUnhandledException(object sender, Exception initialException, bool isTerminating)
{
Logger.Log log = Logger.Error ?? Logger.Notice;
string message = $"Unhandled exception caught: {ex}";
// ReSharper disable once ConstantConditionalAccessQualifier
if (sender?.GetType()?.AsPrettyString() is { } senderName)
log.Print(LogClass.Application, message, senderName);
List<Exception> exceptions = [];
if (initialException is AggregateException ae)
{
exceptions.AddRange(ae.InnerExceptions);
}
else
log.PrintMsg(LogClass.Application, message);
{
exceptions.Add(initialException);
}
foreach (var e in exceptions)
{
string message = $"Unhandled exception caught: {e}";
// ReSharper disable once ConstantConditionalAccessQualifier
if (sender?.GetType()?.AsPrettyString() is { } senderName)
log.Print(LogClass.Application, message, senderName);
else
log.PrintMsg(LogClass.Application, message);
}
if (isTerminating)
Exit();

View File

@ -166,7 +166,6 @@
<EmbeddedResource Include="Assets\UIImages\Logo_GitHub_Light.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" />
<EmbeddedResource Include="Headless\Ryujinx.bmp" LogicalName="HeadlessLogo" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Assets\locales.json" />
@ -174,4 +173,5 @@
<ItemGroup>
<Folder Include="Assets\Fonts\Mono\" />
</ItemGroup>
</Project>

View File

@ -1,17 +1,24 @@
using Avalonia.Controls;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common;
using Ryujinx.HLE;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
using Ryujinx.HLE.UI;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
namespace Ryujinx.Ava.UI.Applet
@ -253,5 +260,59 @@ namespace Ryujinx.Ava.UI.Applet
}
public IDynamicTextInputHandler CreateDynamicTextInputHandler() => new AvaloniaDynamicTextInputHandler(_parent);
public UserProfile ShowPlayerSelectDialog()
{
UserId selected = UserId.Null;
byte[] defaultGuestImage = EmbeddedResources.Read("Ryujinx.HLE/HOS/Services/Account/Acc/GuestUserImage.jpg");
UserProfile guest = new UserProfile(new UserId("00000000000000000000000000000080"), "Guest", defaultGuestImage);
ManualResetEvent dialogCloseEvent = new(false);
Dispatcher.UIThread.InvokeAsync(async () =>
{
ObservableCollection<BaseModel> profiles = [];
NavigationDialogHost nav = new();
_parent.AccountManager.GetAllUsers()
.OrderBy(x => x.Name)
.ForEach(profile => profiles.Add(new Models.UserProfile(profile, nav)));
profiles.Add(new Models.UserProfile(guest, nav));
UserSelectorDialogViewModel viewModel = new()
{
Profiles = profiles,
SelectedUserId = _parent.AccountManager.LastOpenedUser.UserId
};
UserSelectorDialog content = new(viewModel);
(selected, _) = await UserSelectorDialog.ShowInputDialog(content);
dialogCloseEvent.Set();
});
dialogCloseEvent.WaitOne();
UserProfile profile = _parent.AccountManager.LastOpenedUser;
if (selected == guest.UserId)
{
profile = guest;
}
else if (selected == UserId.Null)
{
profile = null;
}
else
{
foreach (UserProfile p in _parent.AccountManager.GetAllUsers())
{
if (p.UserId == selected)
{
profile = p;
break;
}
}
}
return profile;
}
}
}

View File

@ -0,0 +1,121 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Applet.UserSelectorDialog"
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:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
d:DesignHeight="450"
MinWidth="500"
d:DesignWidth="800"
mc:Ignorable="d"
Focusable="True"
x:DataType="viewModels:UserSelectorDialogViewModel">
<UserControl.Resources>
<helpers:BitmapArrayValueConverter x:Key="ByteImage" />
</UserControl.Resources>
<Design.DataContext>
<viewModels:UserSelectorDialogViewModel />
</Design.DataContext>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border
CornerRadius="5"
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
BorderThickness="1">
<ListBox
MaxHeight="300"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="Transparent"
ItemsSource="{Binding Profiles}"
SelectionChanged="ProfilesList_SelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel
HorizontalAlignment="Left"
VerticalAlignment="Center"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Margin" Value="5 5 0 5" />
<Setter Property="CornerRadius" Value="5" />
</Style>
<Style Selector="Rectangle#SelectionIndicator">
<Setter Property="Opacity" Value="0" />
</Style>
</ListBox.Styles>
<ListBox.DataTemplates>
<DataTemplate
DataType="models:UserProfile">
<Grid
PointerEntered="Grid_PointerEntered"
PointerExited="Grid_OnPointerExited">
<Border
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClipToBounds="True"
CornerRadius="5"
Background="{Binding BackgroundColor}">
<StackPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Image
Width="96"
Height="96"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Source="{Binding Image, Converter={StaticResource ByteImage}}" />
<TextBlock
HorizontalAlignment="Stretch"
MaxWidth="90"
Text="{Binding Name}"
TextAlignment="Center"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2"
Margin="5" />
</StackPanel>
</Border>
</Grid>
</DataTemplate>
<DataTemplate
DataType="viewModels:BaseModel">
<Panel
Height="118"
Width="96">
<Panel.Styles>
<Style Selector="Panel">
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
</Style>
</Panel.Styles>
</Panel>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
</Border>
<StackPanel
Grid.Row="1"
Margin="0 24 0 0"
HorizontalAlignment="Left"
Orientation="Horizontal"
Spacing="10">
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,123 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
using UserProfileSft = Ryujinx.HLE.HOS.Services.Account.Acc.UserProfile;
namespace Ryujinx.Ava.UI.Applet
{
public partial class UserSelectorDialog : UserControl, INotifyPropertyChanged
{
public UserSelectorDialogViewModel ViewModel { get; set; }
public UserSelectorDialog(UserSelectorDialogViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
DataContext = ViewModel;
}
private void Grid_PointerEntered(object sender, PointerEventArgs e)
{
if (sender is Grid { DataContext: UserProfile profile })
{
profile.IsPointerOver = true;
}
}
private void Grid_OnPointerExited(object sender, PointerEventArgs e)
{
if (sender is Grid { DataContext: UserProfile profile })
{
profile.IsPointerOver = false;
}
}
private void ProfilesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is ListBox listBox)
{
int selectedIndex = listBox.SelectedIndex;
if (selectedIndex >= 0 && selectedIndex < ViewModel.Profiles.Count)
{
if (ViewModel.Profiles[selectedIndex] is UserProfile userProfile)
{
ViewModel.SelectedUserId = userProfile.UserId;
Logger.Info?.Print(LogClass.UI, $"Selected user: {userProfile.UserId}");
ObservableCollection<BaseModel> newProfiles = [];
foreach (var item in ViewModel.Profiles)
{
if (item is UserProfile originalItem)
{
var profile = new UserProfileSft(originalItem.UserId, originalItem.Name, originalItem.Image);
if (profile.UserId == ViewModel.SelectedUserId)
{
profile.AccountState = AccountState.Open;
}
newProfiles.Add(new UserProfile(profile, new NavigationDialogHost()));
}
}
ViewModel.Profiles = newProfiles;
}
}
}
}
public static async Task<(UserId Id, bool Result)> ShowInputDialog(UserSelectorDialog content)
{
ContentDialog contentDialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],
Content = content,
Padding = new Thickness(0)
};
UserId result = UserId.Null;
bool input = false;
void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs)
{
if (eventArgs.Result == ContentDialogResult.Primary)
{
if (contentDialog.Content is UserSelectorDialog view)
{
result = view.ViewModel.SelectedUserId;
input = true;
}
}
else
{
result = UserId.Null;
input = false;
}
}
contentDialog.Closed += Handler;
await ContentDialogHelper.ShowAsync(contentDialog);
return (result, input);
}
}
}

View File

@ -106,6 +106,10 @@
Click="ExtractApplicationRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
Click="ExtractAocRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />
<MenuItem
Click="ExtractApplicationLogo_Click"
Header="{ext:Locale GameListContextMenuExtractDataLogo}"

View File

@ -3,10 +3,13 @@ using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using LibHac;
using LibHac.Fs;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
@ -14,6 +17,7 @@ using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS;
using SkiaSharp;
using System;
@ -279,6 +283,22 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.Name);
}
public async void ExtractAocRomFs_Click(object sender, RoutedEventArgs args)
{
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return;
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
if (selectedDlc is not null)
{
await ApplicationHelper.ExtractAoc(
viewModel.StorageProvider,
selectedDlc.ContainerPath,
selectedDlc.FileName);
}
}
public async void ExtractApplicationLogo_Click(object sender, RoutedEventArgs args)
{

View File

@ -2,11 +2,10 @@
x:Class="Ryujinx.Ava.UI.Controls.ApplicationListView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
Focusable="True"

View File

@ -0,0 +1,67 @@
<UserControl 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:helpers="using:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:models="using:Ryujinx.Ava.Common.Models"
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.DlcSelectView"
x:DataType="viewModels:DlcSelectViewModel">
<Grid RowDefinitions="*,Auto,*">
<TextBlock
Classes="h1"
Margin="5, -5, 0, 15"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextWrapping="Wrap"
Text="{ext:Locale ExtractAocListHeader}" />
<ScrollViewer Grid.Row="2">
<ListBox
AutoScrollToSelectedItem="False"
SelectionMode="Single"
Background="Transparent"
SelectedItem="{Binding SelectedDlc}"
ItemsSource="{Binding Dlcs}">
<ListBox.DataTemplates>
<DataTemplate
DataType="models:DownloadableContentModel">
<Panel Margin="10" Background="Transparent">
<Grid ColumnDefinitions="*,Auto">
<Grid
Grid.Column="0" ColumnDefinitions="*,Auto">
<TextBlock
Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
MaxLines="2"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis">
<TextBlock.Text>
<MultiBinding Converter="{x:Static helpers:DownloadableContentLabelConverter.Instance}">
<Binding Path="FileName" />
<Binding Path="IsBundled" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock
Grid.Column="1"
Margin="10, 0, 0, 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding TitleIdStr}" />
</Grid>
</Grid>
</Panel>
</DataTemplate>
</ListBox.DataTemplates>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="Transparent" />
</Style>
</ListBox.Styles>
</ListBox>
</ScrollViewer>
</Grid>
</UserControl>

View File

@ -0,0 +1,51 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Controls
{
public partial class DlcSelectView : UserControl
{
public DlcSelectView()
{
InitializeComponent();
}
#nullable enable
public static async Task<DownloadableContentModel?> Show(ulong selectedTitleId, ApplicationLibrary appLibrary)
#nullable disable
{
DlcSelectViewModel viewModel = new(selectedTitleId, appLibrary);
ContentDialog contentDialog = new()
{
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
SecondaryButtonText = string.Empty,
CloseButtonText = string.Empty,
Content = new DlcSelectView { DataContext = viewModel }
};
Style closeButton = new(x => x.Name("CloseButton"));
closeButton.Setters.Add(new Setter(WidthProperty, 80d));
Style closeButtonParent = new(x => x.Name("CommandSpace"));
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
Avalonia.Layout.HorizontalAlignment.Right));
contentDialog.Styles.Add(closeButton);
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
return viewModel.SelectedDlc;
}
}
}

View File

@ -69,7 +69,7 @@ namespace Ryujinx.Ava.UI.Controls
VirtualFileSystem ownerVirtualFileSystem,
HorizonClient ownerHorizonClient)
{
var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
NavigationDialogHost content = new(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
ContentDialog contentDialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],

View File

@ -7,6 +7,7 @@
Title="Ryujinx - Waiting"
SizeToContent="WidthAndHeight"
WindowStartupLocation="CenterOwner"
CanResize="False"
mc:Ignorable="d"
Focusable="True">
<Grid

View File

@ -0,0 +1,48 @@
using CommunityToolkit.Mvvm.Input;
using System;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Helpers
{
#nullable enable
public static class Commands
{
public static RelayCommand Create(Action action)
=> new(action);
public static RelayCommand CreateConditional(Action action, Func<bool> canExecute)
=> new(action, canExecute);
public static RelayCommand<T> Create<T>(Action<T?> action)
=> new(action);
public static RelayCommand<T> CreateConditional<T>(Action<T?> action, Predicate<T?> canExecute)
=> new(action, canExecute);
public static AsyncRelayCommand Create(Func<Task> action)
=> new(action, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand CreateConcurrent(Func<Task> action)
=> new(action, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand CreateSilentFail(Func<Task> action)
=> new(action, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
public static AsyncRelayCommand<T> Create<T>(Func<T?, Task> action)
=> new(action, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand<T> CreateConcurrent<T>(Func<T?, Task> action)
=> new(action, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand<T> CreateSilentFail<T>(Func<T?, Task> action)
=> new(action, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
public static AsyncRelayCommand CreateConditional(Func<Task> action, Func<bool> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand CreateConcurrentConditional(Func<Task> action, Func<bool> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand CreateSilentFailConditional(Func<Task> action, Func<bool> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
public static AsyncRelayCommand<T> CreateConditional<T>(Func<T?, Task> action, Predicate<T?> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand<T> CreateConcurrentConditional<T>(Func<T?, Task> action, Predicate<T?> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand<T> CreateSilentFailConditional<T>(Func<T?, Task> action, Predicate<T?> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
}
}

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

@ -22,5 +22,22 @@ namespace Ryujinx.Ava.UI.Models
FifoStatus = fifoStatus;
ShaderCount = shaderCount;
}
public override bool Equals(object obj)
{
if (obj is not StatusUpdatedEventArgs suea) return false;
return
VSyncMode == suea.VSyncMode &&
VolumeStatus == suea.VolumeStatus &&
DockedMode == suea.DockedMode &&
AspectRatio == suea.AspectRatio &&
GameStatus == suea.GameStatus &&
FifoStatus == suea.FifoStatus &&
ShaderCount == suea.ShaderCount;
}
public override int GetHashCode()
=> HashCode.Combine(VSyncMode, VolumeStatus, AspectRatio, DockedMode, FifoStatus, GameStatus, ShaderCount);
}
}

View File

@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.ViewModels
ThemeManager.ThemeChanged += ThemeManager_ThemeChanged;
}
private void ThemeManager_ThemeChanged(object sender, EventArgs e)
private void ThemeManager_ThemeChanged()
{
Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value));
}

View File

@ -0,0 +1,25 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Linq;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class DlcSelectViewModel : BaseModel
{
[ObservableProperty] private DownloadableContentModel[] _dlcs;
#nullable enable
[ObservableProperty] private DownloadableContentModel? _selectedDlc;
#nullable disable
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
{
_dlcs = appLibrary.DownloadableContents.Items
.Where(x => x.Dlc.TitleIdBase == titleId)
.Select(x => x.Dlc)
.OrderBy(it => it.IsBundled ? 0 : 1)
.ThenBy(it => it.TitleId)
.ToArray();
}
}
}

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

@ -38,7 +38,6 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.UI;
using Ryujinx.Input.HLE;
using Silk.NET.Vulkan;
using SkiaSharp;
using System;
using System.Collections.Generic;

View File

@ -0,0 +1,14 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using System.Collections.ObjectModel;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class UserSelectorDialogViewModel : BaseModel
{
[ObservableProperty] private UserId _selectedUserId;
[ObservableProperty] private ObservableCollection<BaseModel> _profiles = [];
}
}

View File

@ -208,7 +208,7 @@
Name="ActionsMenuItem"
VerticalAlignment="Center"
Header="{ext:Locale MenuBarActions}"
IsEnabled="{Binding IsGameRunning}">
IsVisible="{Binding !EnableNonGameRunningControls}">
<MenuItem
Name="PauseEmulationMenuItem"
Header="{ext:Locale MenuBarOptionsPauseEmulation}"
@ -265,21 +265,21 @@
Icon="{ext:Icon fa-solid fa-code}"
IsEnabled="{Binding IsGameRunning}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarTools}">
<MenuItem Header="{ext:Locale MenuBarToolsInstallKeys}" Icon="{ext:Icon fa-solid fa-key}" IsEnabled="{Binding EnableNonGameRunningControls}">
<MenuItem Command="{Binding InstallKeysFromFile}" Header="{ext:Locale MenuBarFileToolsInstallKeysFromFile}" Icon="{ext:Icon mdi-file-cog}" />
<MenuItem Command="{Binding InstallKeysFromFolder}" Header="{ext:Locale MenuBarFileToolsInstallKeysFromFolder}" Icon="{ext:Icon mdi-folder-cog}" />
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarActions}" IsVisible="{Binding EnableNonGameRunningControls}">
<MenuItem Header="{ext:Locale MenuBarActionsInstallKeys}" Icon="{ext:Icon fa-solid fa-key}">
<MenuItem Command="{Binding InstallKeysFromFile}" Header="{ext:Locale MenuBarFileActionsInstallKeysFromFile}" Icon="{ext:Icon mdi-file-cog}" />
<MenuItem Command="{Binding InstallKeysFromFolder}" Header="{ext:Locale MenuBarFileActionsInstallKeysFromFolder}" Icon="{ext:Icon mdi-folder-cog}" />
</MenuItem>
<MenuItem Header="{ext:Locale MenuBarToolsInstallFirmware}" Icon="{ext:Icon fa-solid fa-download}" IsEnabled="{Binding EnableNonGameRunningControls}">
<MenuItem Command="{Binding InstallFirmwareFromFile}" Header="{ext:Locale MenuBarFileToolsInstallFirmwareFromFile}" Icon="{ext:Icon mdi-file-cog}" />
<MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" Icon="{ext:Icon mdi-folder-cog}" />
<MenuItem Header="{ext:Locale MenuBarActionsInstallFirmware}" Icon="{ext:Icon fa-solid fa-download}">
<MenuItem Command="{Binding InstallFirmwareFromFile}" Header="{ext:Locale MenuBarActionsInstallFirmwareFromFile}" Icon="{ext:Icon mdi-file-cog}" />
<MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarActionsInstallFirmwareFromDirectory}" Icon="{ext:Icon mdi-folder-cog}" />
</MenuItem>
<MenuItem Header="{ext:Locale MenuBarToolsManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}">
<MenuItem Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBarToolsInstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" />
<MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBarToolsUninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" />
<MenuItem Header="{ext:Locale MenuBarActionsManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}">
<MenuItem Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsInstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" />
<MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsUninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" />
</MenuItem>
<Separator />
<MenuItem Name="XciTrimmerMenuItem" Header="{ext:Locale MenuBarToolsXCITrimmer}" IsEnabled="{Binding EnableNonGameRunningControls}" Icon="{ext:Icon fa-solid fa-scissors}" />
<MenuItem Name="XciTrimmerMenuItem" Header="{ext:Locale MenuBarActionsXCITrimmer}" Icon="{ext:Icon fa-solid fa-scissors}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarView}">
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarViewWindow}">

View File

@ -37,20 +37,20 @@ namespace Ryujinx.Ava.UI.Views.Main
ToggleFileTypesMenuItem.ItemsSource = GenerateToggleFileTypeItems();
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
MiiAppletMenuItem.Command = new AsyncRelayCommand(OpenMiiApplet);
CloseRyujinxMenuItem.Command = new RelayCommand(CloseWindow);
OpenSettingsMenuItem.Command = new AsyncRelayCommand(OpenSettings);
PauseEmulationMenuItem.Command = new RelayCommand(() => ViewModel.AppHost?.Pause());
ResumeEmulationMenuItem.Command = new RelayCommand(() => ViewModel.AppHost?.Resume());
StopEmulationMenuItem.Command = new AsyncRelayCommand(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
CheatManagerMenuItem.Command = new AsyncRelayCommand(OpenCheatManagerForCurrentApp);
InstallFileTypesMenuItem.Command = new AsyncRelayCommand(InstallFileTypes);
UninstallFileTypesMenuItem.Command = new AsyncRelayCommand(UninstallFileTypes);
XciTrimmerMenuItem.Command = new AsyncRelayCommand(() => XCITrimmerWindow.Show(ViewModel));
AboutWindowMenuItem.Command = new AsyncRelayCommand(AboutWindow.Show);
CompatibilityListMenuItem.Command = new AsyncRelayCommand(CompatibilityList.Show);
MiiAppletMenuItem.Command = Commands.Create(OpenMiiApplet);
CloseRyujinxMenuItem.Command = Commands.Create(CloseWindow);
OpenSettingsMenuItem.Command = Commands.Create(OpenSettings);
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
StopEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
CheatManagerMenuItem.Command = Commands.CreateSilentFail(OpenCheatManagerForCurrentApp);
InstallFileTypesMenuItem.Command = Commands.Create(InstallFileTypes);
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show);
AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show);
CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show);
UpdateMenuItem.Command = new AsyncRelayCommand(async () =>
UpdateMenuItem.Command = Commands.Create(async () =>
{
if (Updater.CanUpdate(true))
await Updater.BeginUpdateAsync(true);
@ -58,12 +58,12 @@ namespace Ryujinx.Ava.UI.Views.Main
FaqMenuItem.Command =
SetupGuideMenuItem.Command =
LdnGuideMenuItem.Command = new RelayCommand<string>(OpenHelper.OpenUrl);
LdnGuideMenuItem.Command = Commands.Create<string>(OpenHelper.OpenUrl);
WindowSize720PMenuItem.Command =
WindowSize1080PMenuItem.Command =
WindowSize1440PMenuItem.Command =
WindowSize2160PMenuItem.Command = new RelayCommand<string>(ChangeWindowSize);
WindowSize2160PMenuItem.Command = Commands.Create<string>(ChangeWindowSize);
}
private IEnumerable<CheckBox> GenerateToggleFileTypeItems() =>
@ -138,14 +138,14 @@ namespace Ryujinx.Ava.UI.Views.Main
ViewModel.LoadConfigurableHotKeys();
}
public static readonly AppletMetadata MiiApplet = new("miiEdit", 0x0100000000001009);
public AppletMetadata MiiApplet => new(ViewModel.ContentManager, "miiEdit", 0x0100000000001009);
public async Task OpenMiiApplet()
{
if (MiiApplet.CanStart(ViewModel.ContentManager, out var appData, out var nacpData))
{
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData);
}
if (!MiiApplet.CanStart(out var appData, out var nacpData))
return;
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData);
}
public async Task OpenCheatManagerForCurrentApp()

View File

@ -136,6 +136,12 @@
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageBrazilianPortuguese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageSwedish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageNorwegian}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel

View File

@ -14,9 +14,6 @@
mc:Ignorable="d"
x:DataType="viewModels:DownloadableContentManagerViewModel"
Focusable="True">
<UserControl.Resources>
<helpers:DownloadableContentLabelConverter x:Key="DownloadableContentLabel" />
</UserControl.Resources>
<Grid RowDefinitions="Auto,Auto,*,Auto">
<StackPanel
Grid.Row="0"
@ -89,7 +86,7 @@
<ListBox.DataTemplates>
<DataTemplate
DataType="models:DownloadableContentModel">
<Panel Margin="10">
<Panel Margin="10" Background="Transparent">
<Grid ColumnDefinitions="*,Auto">
<Grid
Grid.Column="0" ColumnDefinitions="*,Auto">
@ -101,7 +98,7 @@
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource DownloadableContentLabel}">
<MultiBinding Converter="{x:Static helpers:DownloadableContentLabelConverter.Instance}">
<Binding Path="FileName" />
<Binding Path="IsBundled" />
</MultiBinding>
@ -112,7 +109,7 @@
Margin="10 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding TitleId}" />
Text="{Binding TitleIdStr}" />
</Grid>
<StackPanel
Grid.Column="1"

View File

@ -2,6 +2,8 @@ using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.UI.ViewModels;

View File

@ -32,6 +32,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
@ -136,6 +137,8 @@ namespace Ryujinx.Ava.UI.Windows
base.OnApplyTemplate(e);
NotificationHelper.SetNotificationManager(this);
Executor.ExecuteBackgroundAsync(ShowIntelMacWarningAsync);
}
private void OnScalingChanged(object sender, EventArgs e)
@ -164,7 +167,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 +411,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()
@ -734,5 +734,20 @@ namespace Ryujinx.Ava.UI.Windows
(int)Symbol.Checkmark);
});
}
private static bool _intelMacWarningShown = !(OperatingSystem.IsMacOS() &&
(RuntimeInformation.OSArchitecture == Architecture.X64 ||
RuntimeInformation.OSArchitecture == Architecture.X86));
public static async Task ShowIntelMacWarningAsync()
{
if (_intelMacWarningShown) return;
await Dispatcher.UIThread.InvokeAsync(async () => await ContentDialogHelper.CreateWarningDialog(
"Intel Mac Warning",
"Intel Macs are not supported and will not work properly.\nIf you continue, do not come to our Discord asking for support;\nand do not report bugs on the GitHub. They will be closed."));
_intelMacWarningShown = true;
}
}
}

View File

@ -28,14 +28,14 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent();
}
public static async Task Show(MainWindowViewModel mainWindowViewModel)
public static async Task Show()
{
ContentDialog contentDialog = new()
{
PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty,
CloseButtonText = string.Empty,
Content = new XCITrimmerWindow(mainWindowViewModel),
Content = new XCITrimmerWindow(RyujinxApp.MainWindow.ViewModel),
Title = string.Format(LocaleManager.Instance[LocaleKeys.XCITrimmerWindowTitle]),
};

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

@ -54,5 +54,9 @@ namespace Ryujinx.Ava.Utilities
appControl = new BlitStruct<ApplicationControlProperty>(0);
return false;
}
public bool CanStart(out ApplicationData appData,
out BlitStruct<ApplicationControlProperty> appControl)
=> CanStart(null, out appData, out appControl);
}
}

View File

@ -28,7 +28,9 @@ namespace Ryujinx.Ava.Utilities.Compat
public class CompatibilityCsv
{
static CompatibilityCsv()
static CompatibilityCsv() => Load();
public static void Load()
{
using Stream csvStream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("RyujinxGameCompatibilityList")!;
@ -37,15 +39,31 @@ namespace Ryujinx.Ava.Utilities.Compat
using SepReader reader = Sep.Reader().From(csvStream);
ColumnIndices columnIndices = new(reader.Header.IndexOf);
Entries = reader
_entries = reader
.Enumerate(row => new CompatibilityEntry(ref columnIndices, row))
.OrderBy(it => it.GameName)
.ToArray();
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatCsv");
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatibility");
}
public static CompatibilityEntry[] Entries { get; private set; }
public static void Unload()
{
_entries = null;
}
private static CompatibilityEntry[] _entries;
public static CompatibilityEntry[] Entries
{
get
{
if (_entries == null)
Load();
return _entries;
}
}
}
public class CompatibilityEntry
@ -57,7 +75,7 @@ namespace Ryujinx.Ava.Utilities.Compat
? titleIdRow
: default(Optional<string>);
GameName = ColStr(row[indices.GameName]).Trim().Trim('"');
GameName = ColStr(row[indices.GameName]);
Labels = ColStr(row[indices.Labels]).Split(';');
Status = ColStr(row[indices.Status]).ToLower() switch
@ -92,7 +110,6 @@ namespace Ryujinx.Ava.Utilities.Compat
.OrElse(new string(' ', 16));
public string FormattedIssueLabels => Labels
.Where(it => !it.StartsWithIgnoreCase("status"))
.Select(FormatLabelName)
.JoinToString(", ");

View File

@ -1,11 +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;
using System.Threading.Tasks;
namespace Ryujinx.Ava.Utilities.Compat
@ -35,6 +32,8 @@ namespace Ryujinx.Ava.Utilities.Compat
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
CompatibilityCsv.Unload();
}
public CompatibilityList()

View File

@ -1,19 +1,18 @@
using CommunityToolkit.Mvvm.ComponentModel;
using ExCSS;
using Gommon;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Ava.Utilities.Compat
{
public partial class CompatibilityViewModel : ObservableObject
public class CompatibilityViewModel : BaseModel
{
[ObservableProperty] private bool _onlyShowOwnedGames = true;
private bool _onlyShowOwnedGames = true;
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityCsv.Entries;
private readonly string[] _ownedGameTitleIds = [];
private readonly ApplicationLibrary _appLibrary;
private string[] _ownedGameTitleIds = [];
public IEnumerable<CompatibilityEntry> CurrentEntries => OnlyShowOwnedGames
? _currentEntries.Where(x =>
@ -24,14 +23,23 @@ namespace Ryujinx.Ava.Utilities.Compat
public CompatibilityViewModel(ApplicationLibrary appLibrary)
{
_appLibrary = appLibrary;
appLibrary.ApplicationCountUpdated += (_, _)
=> _ownedGameTitleIds = appLibrary.Applications.Keys.Select(x => x.ToString("X16")).ToArray();
_ownedGameTitleIds = appLibrary.Applications.Keys.Select(x => x.ToString("X16")).ToArray();
}
PropertyChanged += (_, args) =>
public bool OnlyShowOwnedGames
{
get => _onlyShowOwnedGames;
set
{
if (args.PropertyName is nameof(OnlyShowOwnedGames))
OnPropertyChanged(nameof(CurrentEntries));
};
OnPropertyChanging();
OnPropertyChanging(nameof(CurrentEntries));
_onlyShowOwnedGames = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CurrentEntries));
}
}
public void Search(string searchTerm)

View File

@ -4,7 +4,7 @@ namespace Ryujinx.Ava.Utilities
{
public static class TitleHelper
{
public static string ActiveApplicationTitle(ProcessResult activeProcess, string applicationVersion, string pauseString = "")
public static string ActiveApplicationTitle(ProcessResult activeProcess, string applicationVersion, bool customTitlebar, string pauseString = "")
{
if (activeProcess == null)
return string.Empty;
@ -14,7 +14,9 @@ namespace Ryujinx.Ava.Utilities
string titleIdSection = $" ({activeProcess.ProgramIdText.ToUpper()})";
string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
string appTitle = $"Ryujinx {applicationVersion} -{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
string appTitle = customTitlebar
? $"Ryujinx {applicationVersion}\n{titleNameSection.Trim()}\n{titleVersionSection.Trim()}\n{titleIdSection.Trim()}{titleArchSection}"
: $"Ryujinx {applicationVersion} -{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
return !string.IsNullOrEmpty(pauseString)
? appTitle + $" ({pauseString})"