Compare commits

..

2 Commits

Author SHA1 Message Date
Frog Business
d81b1b36e7 Merge 6d78e71fc7 into fafb99c702 2025-02-04 23:42:26 +01:00
Barış Hamil
6d78e71fc7 Handheld Gyro 2025-02-01 22:26:05 +03:00
32 changed files with 368 additions and 575 deletions

View File

@@ -3,6 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Alimer.Bindings.SDL" Version="3.7.1" />
<PackageVersion Include="Avalonia" Version="11.0.13" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.13" />

View File

@@ -75,6 +75,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon", "src\Ryuj
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Generators", "src\Ryujinx.Horizon.Kernel.Generators\Ryujinx.Horizon.Kernel.Generators.csproj", "{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input.SDL3", "src\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj", "{3BF24278-547D-42C2-9D43-182B978F54DD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.Metal", "src\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj", "{C08931FA-1191-417A-864F-3882D93E683B}"
@@ -259,6 +261,10 @@ Global
{81EA598C-DBA1-40B0-8DA4-4796B78F2037}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81EA598C-DBA1-40B0-8DA4-4796B78F2037}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81EA598C-DBA1-40B0-8DA4-4796B78F2037}.Release|Any CPU.Build.0 = Release|Any CPU
{3BF24278-547D-42C2-9D43-182B978F54DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3BF24278-547D-42C2-9D43-182B978F54DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3BF24278-547D-42C2-9D43-182B978F54DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3BF24278-547D-42C2-9D43-182B978F54DD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -56,6 +56,7 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
return motionBackendType switch
{
MotionInputBackendType.GamepadDriver => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardMotionConfigController),
MotionInputBackendType.Handheld => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardMotionConfigController),
MotionInputBackendType.CemuHook => JsonSerializer.Deserialize(ref reader, _serializerContext.CemuHookMotionConfigController),
_ => throw new InvalidOperationException($"Unknown backend type {motionBackendType}"),
};
@@ -66,6 +67,7 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
switch (value.MotionBackend)
{
case MotionInputBackendType.GamepadDriver:
case MotionInputBackendType.Handheld:
JsonSerializer.Serialize(writer, value as StandardMotionConfigController, _serializerContext.StandardMotionConfigController);
break;
case MotionInputBackendType.CemuHook:

View File

@@ -9,5 +9,6 @@ namespace Ryujinx.Common.Configuration.Hid.Controller.Motion
Invalid,
GamepadDriver,
CemuHook,
Handheld,
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<PackageReference Include="Alimer.Bindings.SDL" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,85 @@
using SDL3;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using static SDL3.SDL3;
namespace Ryujinx.Input.SDL3
{
public unsafe class SDL3MotionDriver : IHandheld, IDisposable
{
private readonly Dictionary<SDL_SensorType, SDL_Sensor> sensors;
private bool _disposed;
public SDL3MotionDriver()
{
int result = SDL_Init(SDL_InitFlags.Sensor);
if (result < 0)
{
throw new InvalidOperationException($"SDL sensor initialization failed: {SDL_GetError()}");
}
sensors = SDL_GetSensors().ToArray().ToDictionary(SDL_GetSensorTypeForID, SDL_OpenSensor);
}
~SDL3MotionDriver()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing && sensors != null)
{
foreach (var sensor in sensors.Values)
{
if (sensor != IntPtr.Zero)
{
SDL_CloseSensor(sensor);
}
}
}
_disposed = true;
}
public Vector3 GetMotionData(MotionInputId inputType)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return inputType switch
{
MotionInputId.Gyroscope => GetSensorVector(SDL_SensorType.Gyro) * 180 / MathF.PI,
MotionInputId.Accelerometer => GetSensorVector(SDL_SensorType.Accel) / SDL_STANDARD_GRAVITY,
_ => Vector3.Zero
};
}
private Vector3 GetSensorVector(SDL_SensorType sensorType)
{
if (!sensors.TryGetValue(sensorType, out SDL_Sensor sensor))
{
return Vector3.Zero;
}
var data = stackalloc float[3];
if (SDL_GetSensorData(sensor, data, 3) < 0)
{
return Vector3.Zero;
}
return new Vector3(data[0], data[1], data[2]);
}
}
}

View File

@@ -2,12 +2,13 @@ using System;
namespace Ryujinx.Input.HLE
{
public class InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver)
public class InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IHandheld handheld)
: IDisposable
{
public IGamepadDriver KeyboardDriver { get; } = keyboardDriver;
public IGamepadDriver GamepadDriver { get; } = gamepadDriver;
public IGamepadDriver MouseDriver { get; private set; }
public IHandheld Handheld { get; } = handheld;
public void SetMouseDriver(IGamepadDriver mouseDriver)
{
@@ -18,7 +19,7 @@ namespace Ryujinx.Input.HLE
public NpadManager CreateNpadManager()
{
return new NpadManager(KeyboardDriver, GamepadDriver, MouseDriver);
return new NpadManager(KeyboardDriver, GamepadDriver, MouseDriver, Handheld);
}
public TouchScreenManager CreateTouchScreenManager()
@@ -38,6 +39,7 @@ namespace Ryujinx.Input.HLE
KeyboardDriver?.Dispose();
GamepadDriver?.Dispose();
MouseDriver?.Dispose();
Handheld?.Dispose();
}
}

View File

@@ -218,12 +218,14 @@ namespace Ryujinx.Input.HLE
public string Id { get; private set; }
private readonly CemuHookClient _cemuHookClient;
private readonly IHandheld _handheld;
public NpadController(CemuHookClient cemuHookClient)
public NpadController(CemuHookClient cemuHookClient, IHandheld handheld)
{
State = default;
Id = null;
_cemuHookClient = cemuHookClient;
_handheld = handheld;
}
public bool UpdateDriverConfiguration(IGamepadDriver gamepadDriver, InputConfig config)
@@ -287,6 +289,18 @@ namespace Ryujinx.Input.HLE
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Motion.EnableMotion)
{
if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.Handheld)
{
Vector3 accelerometer = _handheld.GetMotionData(MotionInputId.Accelerometer);
Vector3 gyroscope = _handheld.GetMotionData(MotionInputId.Gyroscope);
accelerometer = new Vector3(accelerometer.X, -accelerometer.Z, accelerometer.Y);
gyroscope = new Vector3(gyroscope.X, -gyroscope.Z, gyroscope.Y);
_leftMotionInput.Update(accelerometer, gyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
_rightMotionInput = _leftMotionInput;
}
if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver)
{
if (gamepad.Features.HasFlag(GamepadFeaturesFlag.Motion))

View File

@@ -31,6 +31,7 @@ namespace Ryujinx.Input.HLE
private readonly IGamepadDriver _keyboardDriver;
private readonly IGamepadDriver _gamepadDriver;
private readonly IGamepadDriver _mouseDriver;
private readonly IHandheld _handheld;
private bool _isDisposed;
private List<InputConfig> _inputConfig;
@@ -38,7 +39,7 @@ namespace Ryujinx.Input.HLE
private bool _enableMouse;
private Switch _device;
public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver)
public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver, IHandheld handheld)
{
_controllers = new NpadController[MaxControllers];
_cemuHookClient = new CemuHookClient(this);
@@ -47,6 +48,7 @@ namespace Ryujinx.Input.HLE
_gamepadDriver = gamepadDriver;
_mouseDriver = mouseDriver;
_inputConfig = [];
_handheld = handheld;
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -139,7 +141,7 @@ namespace Ryujinx.Input.HLE
}
else
{
controller = new(_cemuHookClient);
controller = new(_cemuHookClient, _handheld);
}
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);

View File

@@ -0,0 +1,10 @@
using System;
using System.Numerics;
namespace Ryujinx.Input
{
public interface IHandheld : IDisposable
{
Vector3 GetMotionData(MotionInputId gyroscope);
}
}

View File

@@ -1525,151 +1525,151 @@
{
"ID": "GameListHeaderDeveloper",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Developed by {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": ""
"ar_SA": "المطور",
"de_DE": "Entwickler",
"el_GR": "Προγραμματιστής",
"en_US": "Developer",
"es_ES": "Desarrollador",
"fr_FR": "Développeur",
"he_IL": "מפתח",
"it_IT": "Sviluppatore",
"ja_JP": "開発元",
"ko_KR": "개발자",
"no_NO": "Utvikler",
"pl_PL": "Twórca",
"pt_BR": "Desenvolvedor",
"ru_RU": "Разработчик",
"sv_SE": "Utvecklare",
"th_TH": "ผู้พัฒนา",
"tr_TR": "Geliştirici",
"uk_UA": "Розробник",
"zh_CN": "制作商",
"zh_TW": "開發者"
}
},
{
"ID": "GameListHeaderVersion",
"Translations": {
"ar_SA": "",
"ar_SA": "الإصدار",
"de_DE": "",
"el_GR": "Έκδοση: {0}",
"en_US": "Version: {0}",
"es_ES": "Versión: {0}",
"el_GR": "Έκδοση",
"en_US": "Version",
"es_ES": "Versión",
"fr_FR": "",
"he_IL": "",
"it_IT": "Versione: {0}",
"ja_JP": "バージョン: {0}",
"ko_KR": "버전: {0}",
"no_NO": "Versjon: {0}",
"pl_PL": "Wersja: {0}",
"pt_BR": "Versão: {0}",
"ru_RU": "Версия: {0}",
"he_IL": "גרסה",
"it_IT": "Versione",
"ja_JP": "バージョン",
"ko_KR": "버전",
"no_NO": "Versjon",
"pl_PL": "Wersja",
"pt_BR": "Versão",
"ru_RU": "Версия",
"sv_SE": "",
"th_TH": "เวอร์ชั่น: {0}",
"tr_TR": "Sürüm: {0}",
"uk_UA": "Версія: {0}",
"zh_CN": "版本: {0}",
"zh_TW": "版本: {0}"
"th_TH": "เวอร์ชั่น",
"tr_TR": "Sürüm",
"uk_UA": "Версія",
"zh_CN": "版本",
"zh_TW": "版本"
}
},
{
"ID": "GameListHeaderTimePlayed",
"Translations": {
"ar_SA": "",
"de_DE": "Spielzeit: {0}",
"el_GR": "Χρόνος: {0}",
"en_US": "Play Time: {0}",
"es_ES": "Tiempo jugado: {0}",
"fr_FR": "Temps de jeu: {0}",
"he_IL": "",
"it_IT": "Tempo di gioco: {0}",
"ja_JP": "プレイ時間: {0}",
"ko_KR": "플레이 타임: {0}",
"no_NO": "Spilletid: {0}",
"pl_PL": "Czas w grze: {0}",
"pt_BR": "Tempo de jogo: {0}",
"ru_RU": "Время в игре: {0}",
"sv_SE": "Speltid: {0}",
"th_TH": "เล่นไปแล้ว: {0}",
"tr_TR": "Oynama Süresi: {0}",
"uk_UA": "Зіграно часу: {0}",
"zh_CN": "游玩时长: {0}",
"zh_TW": "遊玩時數: {0}"
"ar_SA": "وقت اللعب",
"de_DE": "Spielzeit",
"el_GR": "Χρόνος",
"en_US": "Play Time",
"es_ES": "Tiempo jugado",
"fr_FR": "Temps de jeu",
"he_IL": "זמן משחק",
"it_IT": "Tempo di gioco",
"ja_JP": "プレイ時間",
"ko_KR": "플레이 타임",
"no_NO": "Spilletid",
"pl_PL": "Czas w grze:",
"pt_BR": "Tempo de jogo",
"ru_RU": "Время в игре",
"sv_SE": "Speltid",
"th_TH": "เล่นไปแล้ว",
"tr_TR": "Oynama Süresi",
"uk_UA": "Зіграно часу",
"zh_CN": "游玩时长",
"zh_TW": "遊玩時數"
}
},
{
"ID": "GameListHeaderLastPlayed",
"Translations": {
"ar_SA": "",
"de_DE": "Zuletzt gespielt: {0}",
"el_GR": "Παίχτηκε: {0}",
"en_US": "Last Played: {0}",
"es_ES": "Jugado por última vez: {0}",
"fr_FR": "Dernière partie jouée: {0}",
"he_IL": "",
"it_IT": "Ultima partita: {0}",
"ja_JP": "最終プレイ日時: {0}",
"ko_KR": "마지막 플레이: {0}",
"no_NO": "Sist Spilt: {0}",
"pl_PL": "Ostatnio grane: {0}",
"pt_BR": "Último jogo: {0}",
"ru_RU": "Последний запуск: {0}",
"sv_SE": "Senast spelad: {0}",
"th_TH": "เล่นล่าสุด: {0}",
"tr_TR": "Son Oynama Tarihi: {0}",
"uk_UA": "Востаннє зіграно: {0}",
"zh_CN": "最近游玩: {0}",
"zh_TW": "最近遊玩: {0}"
"ar_SA": "آخر مرة لُعبت",
"de_DE": "Zuletzt gespielt",
"el_GR": "Παίχτηκε",
"en_US": "Last Played",
"es_ES": "Jugado por última vez",
"fr_FR": "Dernière partie jouée",
"he_IL": "שוחק לאחרונה",
"it_IT": "Ultima partita",
"ja_JP": "最終プレイ日時",
"ko_KR": "마지막 플레이",
"no_NO": "Sist Spilt",
"pl_PL": "Ostatnio grane",
"pt_BR": "Último jogo",
"ru_RU": "Последний запуск",
"sv_SE": "Senast spelad",
"th_TH": "เล่นล่าสุด",
"tr_TR": "Son Oynama Tarihi",
"uk_UA": "Востаннє зіграно",
"zh_CN": "最近游玩",
"zh_TW": "最近遊玩"
}
},
{
"ID": "GameListHeaderFileExtension",
"Translations": {
"ar_SA": "",
"de_DE": "Dateiformat: {0}",
"el_GR": "Κατάληξη: {0}",
"en_US": "Extension: {0}",
"es_ES": "Extensión: {0}",
"fr_FR": "Extension du Fichier: {0}",
"he_IL": "",
"it_IT": "Estensione: {0}",
"ja_JP": "ファイル拡張子: {0}",
"ko_KR": "파일 확장자: {0}",
"no_NO": "Fil Eks.: {0}",
"pl_PL": "Rozszerzenie pliku: {0}",
"pt_BR": "Extensão: {0}",
"ru_RU": "Расширение файла: {0}",
"sv_SE": "Filänd: {0}",
"th_TH": "นามสกุลไฟล์: {0}",
"tr_TR": "Dosya Uzantısı: {0}",
"uk_UA": "Розширення файлу: {0}",
"zh_CN": "扩展名: {0}",
"zh_TW": "副檔名: {0}"
"ar_SA": "صيغة الملف",
"de_DE": "Dateiformat",
"el_GR": "Κατάληξη",
"en_US": "File Ext",
"es_ES": "Extensión",
"fr_FR": "Extension du Fichier",
"he_IL": "סיומת קובץ",
"it_IT": "Estensione",
"ja_JP": "ファイル拡張子",
"ko_KR": "파일 확장자",
"no_NO": "Fil Eks.",
"pl_PL": "Rozszerzenie pliku",
"pt_BR": "Extensão",
"ru_RU": "Расширение файла",
"sv_SE": "Filänd",
"th_TH": "นามสกุลไฟล์",
"tr_TR": "Dosya Uzantısı",
"uk_UA": "Розширення файлу",
"zh_CN": "扩展名",
"zh_TW": "副檔名"
}
},
{
"ID": "GameListHeaderFileSize",
"Translations": {
"ar_SA": "",
"de_DE": "Dateigröße: {0}",
"el_GR": "Μέγεθος Αρχείου: {0}",
"en_US": "File Size: {0}",
"es_ES": "Tamaño del archivo: {0}",
"fr_FR": "Taille du Fichier: {0}",
"he_IL": "",
"it_IT": "Dimensione file: {0}",
"ja_JP": "ファイルサイズ: {0}",
"ko_KR": "파일 크기: {0}",
"no_NO": "Fil Størrelse: {0}",
"pl_PL": "Rozmiar pliku: {0}",
"pt_BR": "Tamanho: {0}",
"ru_RU": "Размер файла: {0}",
"sv_SE": "Filstorlek: {0}",
"th_TH": "ขนาดไฟล์: {0}",
"tr_TR": "Dosya Boyutu: {0}",
"uk_UA": "Розмір файлу: {0}",
"zh_CN": "大小: {0}",
"zh_TW": "檔案大小: {0}"
"ar_SA": "حجم الملف",
"de_DE": "Dateigröße",
"el_GR": "Μέγεθος Αρχείου",
"en_US": "File Size",
"es_ES": "Tamaño del archivo",
"fr_FR": "Taille du Fichier",
"he_IL": "גודל הקובץ",
"it_IT": "Dimensione file",
"ja_JP": "ファイルサイズ",
"ko_KR": "파일 크기",
"no_NO": "Fil Størrelse",
"pl_PL": "Rozmiar pliku",
"pt_BR": "Tamanho",
"ru_RU": "Размер файла",
"sv_SE": "Filstorlek",
"th_TH": "ขนาดไฟล์",
"tr_TR": "Dosya Boyutu",
"uk_UA": "Розмір файлу",
"zh_CN": "大小",
"zh_TW": "檔案大小"
}
},
{
@@ -1697,106 +1697,6 @@
"zh_TW": "路徑"
}
},
{
"ID": "GameListHeaderCompatibilityStatus",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Compatibility:",
"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": "GameListHeaderTitleId",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Title ID:",
"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": "GameListHeaderHostedGames",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Hosted Games: {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": "GameListHeaderPlayerCount",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Online Players: {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": "GameListContextMenuOpenUserSaveDirectory",
"Translations": {
@@ -2672,56 +2572,6 @@
"zh_TW": ""
}
},
{
"ID": "GameListContextMenuShowGameData",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show Game Info",
"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": "GameListContextMenuShowGameDataToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"el_GR": "",
"en_US": "Show stats & details about the currently selected game.",
"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": "GameListContextMenuOpenModsDirectory",
"Translations": {
@@ -7722,6 +7572,31 @@
"zh_TW": "使用與 CemuHook 相容的體感"
}
},
{
"ID": "ControllerSettingsMotionUseHandheldCompatibleMotion",
"Translations": {
"ar_SA": "استخدام الحركة المتوافقة مع Hendheld",
"de_DE": "Hendheld kompatible Bewegungssteuerung",
"el_GR": "Κίνηση συμβατή με Hendheld",
"en_US": "Use Hendheld compatible motion",
"es_ES": "Usar movimiento compatible con Hendheld",
"fr_FR": "Utiliser un capteur de mouvements Hendheld",
"he_IL": "השתמש בתנועת Hendheld תואמת ",
"it_IT": "Usa sensore compatibile con Hendheld",
"ja_JP": "Hendheld 互換モーションを使用",
"ko_KR": "Hendheld 호환 모션 사용",
"no_NO": "Bruk Hendheld kompatibel bevegelse",
"pl_PL": "Użyj ruchu zgodnego z Hendheld",
"pt_BR": "Usar sensor compatível com Hendheld",
"ru_RU": "Включить совместимость с Hendheld",
"sv_SE": "Använd Hendheld-kompatibel rörelse",
"th_TH": "ใช้การเคลื่อนไหวที่เข้ากันได้กับ Hendheld",
"tr_TR": "Hendheld uyumlu hareket kullan",
"uk_UA": "Використовувати рух, сумісний з Hendheld",
"zh_CN": "使用 Hendheld 兼容的体感协议",
"zh_TW": "使用與 Hendheld 相容的體感"
}
},
{
"ID": "ControllerSettingsMotionControllerSlot",
"Translations": {
@@ -23398,4 +23273,4 @@
}
}
]
}
}

View File

@@ -23,6 +23,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL2;
using Ryujinx.Input.SDL3;
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;
@@ -182,7 +183,7 @@ namespace Ryujinx.Headless
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
_userChannelPersistence = new UserChannelPersistence();
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver(), new SDL3MotionDriver());
GraphicsConfig.EnableShaderCache = !option.DisableShaderCache;

View File

@@ -76,6 +76,7 @@
<ProjectReference Include="..\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />

View File

@@ -32,9 +32,6 @@ namespace Ryujinx.Ava
public static MainWindow MainWindow => Current!
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>()
.MainWindow.Cast<MainWindow>();
public static IClassicDesktopStyleApplicationLifetime AppLifetime => Current!
.ApplicationLifetime.Cast<IClassicDesktopStyleApplicationLifetime>();
public static bool IsClipboardAvailable(out IClipboard clipboard)
{

View File

@@ -25,11 +25,6 @@
Header="{ext:Locale GameListContextMenuShowCompatEntry}"
Icon="{ext:Icon mdi-gamepad}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
<MenuItem
Click="OpenApplicationData_Click"
Header="{ext:Locale GameListContextMenuShowGameData}"
Icon="{ext:Icon mdi-chart-line}"
ToolTip.Tip="{ext:Locale GameListContextMenuShowGameDataToolTip}"/>
<Separator />
<MenuItem
Click="OpenUserSaveDirectory_Click"
@@ -122,7 +117,6 @@
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
IsVisible="{Binding HasDlc}"
Click="ExtractAocRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />

View File

@@ -334,7 +334,7 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return;
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.Id, viewModel.ApplicationLibrary);
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
if (selectedDlc is not null)
{
@@ -392,12 +392,6 @@ namespace Ryujinx.Ava.UI.Controls
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await CompatibilityList.Show(viewModel.SelectedApplication.IdString);
}
public async void OpenApplicationData_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
await ApplicationDataView.Show(viewModel.SelectedApplication);
}
public async void RunApplication_Click(object sender, RoutedEventArgs args)
{
@@ -407,8 +401,12 @@ namespace Ryujinx.Ava.UI.Controls
public async void TrimXCI_Click(object sender, RoutedEventArgs args)
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
MainWindowViewModel viewModel = (sender as MenuItem)?.DataContext as MainWindowViewModel;
if (viewModel?.SelectedApplication != null)
{
await viewModel.TrimXCIFile(viewModel.SelectedApplication.Path);
}
}
}
}

View File

@@ -1,114 +0,0 @@
<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:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.ApplicationDataView"
x:DataType="viewModels:ApplicationDataViewModel">
<StackPanel Orientation="Horizontal">
<Image Margin="0"
MaxWidth="256"
MinWidth="256"
Source="{Binding AppData.Icon, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
<Border Margin="5, 0" Width="1" Height="256" BorderBrush="Gray" Background="Gray" />
<StackPanel Orientation="Vertical">
<Grid
RowDefinitions="Auto,Auto,Auto"
ColumnDefinitions="*">
<StackPanel Grid.Row="0">
<TextBlock HorizontalAlignment="Left"
Text="{Binding FormattedVersion}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock HorizontalAlignment="Left"
Text="{Binding FormattedDeveloper}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock HorizontalAlignment="Stretch"
Text="{Binding FormattedFileExtension}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock HorizontalAlignment="Stretch"
Text="{Binding FormattedFileSize}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel>
<Separator Grid.Row="1" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<StackPanel Grid.Row="2"
HorizontalAlignment="Left"
Orientation="Vertical"
Spacing="5">
<StackPanel Orientation="Horizontal">
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderCompatibilityStatus}" />
<Button
Click="PlayabilityStatus_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
IsVisible="{Binding AppData.HasPlayabilityInfo}"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
Tag="{Binding AppData.IdString}"
Text="{Binding AppData.LocalizedStatus}"
Foreground="{Binding AppData.PlayabilityStatus, Converter={x:Static helpers:PlayabilityStatusConverter.Shared}}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Button.Styles>
<Style Selector="Button">
<Setter Property="MinWidth"
Value="0" />
<!-- avoids very wide buttons from the overall project avalonia style -->
</Style>
</Button.Styles>
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Padding="0, 0, 5, 0" Text="{ext:Locale GameListHeaderTitleId}" />
<Button
Click="IdString_OnClick"
HorizontalContentAlignment="Left"
VerticalAlignment="Center"
Background="{DynamicResource AppListBackgroundColor}"
Padding="0">
<TextBlock
Margin="1.5"
HorizontalAlignment="Stretch"
Text="{Binding AppData.IdString}"
TextAlignment="Start"
TextWrapping="Wrap" />
</Button>
</StackPanel>
</StackPanel>
</Grid>
<Separator Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<TextBlock
HorizontalAlignment="Stretch"
IsVisible="{Binding AppData.HasLdnGames}"
Text="{Binding FormattedLdnInfo}"
TextAlignment="Start"
TextWrapping="Wrap" />
<Separator IsVisible="{Binding AppData.HasLdnGames}" Margin="0, 10, 0, 10" Height="1" BorderBrush="Gray" Background="Gray" />
<StackPanel
HorizontalAlignment="Left"
VerticalAlignment="Top"
Orientation="Vertical"
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FormattedLastPlayed}"
TextAlignment="Start"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding FormattedPlayTime}"
IsVisible="{Binding AppData.HasPlayedPreviously}"
TextAlignment="Start"
TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

View File

@@ -1,86 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Compat;
using System.Linq;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Controls
{
public partial class ApplicationDataView : UserControl
{
public static async Task Show(ApplicationData appData)
{
ContentDialog contentDialog = new()
{
Title = appData.Name,
PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty,
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
MinWidth = 256,
Content = new ApplicationDataView { DataContext = new ApplicationDataViewModel(appData) }
};
Style closeButton = new(x => x.Name("CloseButton"));
closeButton.Setters.Add(new Setter(WidthProperty, 160d));
Style closeButtonParent = new(x => x.Name("CommandSpace"));
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
Avalonia.Layout.HorizontalAlignment.Center));
contentDialog.Styles.Add(closeButton);
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
}
public ApplicationDataView()
{
InitializeComponent();
}
private async void PlayabilityStatus_OnClick(object sender, RoutedEventArgs e)
{
if (sender is not Button { Content: TextBlock playabilityLabel })
return;
if (RyujinxApp.AppLifetime.Windows.TryGetFirst(x => x is ContentDialogOverlayWindow, out Window window))
window.Close(ContentDialogResult.None);
await CompatibilityList.Show((string)playabilityLabel.Tag);
}
private async void IdString_OnClick(object sender, RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel mwvm)
return;
if (sender is not Button { Content: TextBlock idText })
return;
if (!RyujinxApp.IsClipboardAvailable(out IClipboard clipboard))
return;
ApplicationData appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text);
if (appData is null)
return;
await clipboard.SetTextAsync(appData.IdString);
NotificationHelper.ShowInformation(
"Copied Title ID",
$"{appData.Name} ({appData.IdString})");
}
}
}

View File

@@ -140,7 +140,6 @@
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
IsVisible="{Binding HasLdnGames}"
Text="{Binding Converter={helpers:MultiplayerInfoConverter}}"
TextAlignment="Start"
TextWrapping="Wrap"/>

View File

@@ -1,11 +1,8 @@
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.AppLibrary;
using System;
using System.Globalization;
using System.Text;
namespace Ryujinx.Ava.UI.Helpers
{
@@ -15,17 +12,16 @@ namespace Ryujinx.Ava.UI.Helpers
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not ApplicationData { HasLdnGames: true } applicationData)
return "";
if (value is ApplicationData applicationData)
{
if (applicationData.PlayerCount != 0 && applicationData.GameCount != 0)
{
return $"Hosted Games: {applicationData.GameCount}\nOnline Players: {applicationData.PlayerCount}";
}
}
return "";
return new StringBuilder()
.AppendLine(
LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames]
.Format(applicationData.GameCount))
.Append(
LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount]
.Format(applicationData.PlayerCount))
.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

View File

@@ -10,6 +10,7 @@ namespace Ryujinx.Ava.UI.Models.Input
public partial class GamepadInputConfig : BaseModel
{
public bool EnableCemuHookMotion { get; set; }
public bool EnableHandheldMotion { get; set; }
public string DsuServerHost { get; set; }
public int DsuServerPort { get; set; }
public int Slot { get; set; }
@@ -162,7 +163,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableMotion = controllerInput.Motion.EnableMotion;
GyroDeadzone = controllerInput.Motion.GyroDeadzone;
Sensitivity = controllerInput.Motion.Sensitivity;
EnableHandheldMotion = controllerInput.Motion.MotionBackend == MotionInputBackendType.Handheld;
if (controllerInput.Motion is CemuHookMotionConfigController cemuHook)
{
EnableCemuHookMotion = true;
@@ -285,7 +286,7 @@ namespace Ryujinx.Ava.UI.Models.Input
config.Motion = new StandardMotionConfigController
{
EnableMotion = EnableMotion,
MotionBackend = MotionInputBackendType.GamepadDriver,
MotionBackend = EnableHandheldMotion ? MotionInputBackendType.Handheld : MotionInputBackendType.GamepadDriver,
GyroDeadzone = GyroDeadzone,
Sensitivity = Sensitivity,
};

View File

@@ -1,26 +0,0 @@
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Utilities.AppLibrary;
namespace Ryujinx.Ava.UI.ViewModels
{
public class ApplicationDataViewModel : BaseModel
{
public ApplicationData AppData { get; }
public ApplicationDataViewModel(ApplicationData appData) => AppData = appData;
public string FormattedVersion => LocaleManager.Instance[LocaleKeys.GameListHeaderVersion].Format(AppData.Version);
public string FormattedDeveloper => LocaleManager.Instance[LocaleKeys.GameListHeaderDeveloper].Format(AppData.Developer);
public string FormattedFileExtension => LocaleManager.Instance[LocaleKeys.GameListHeaderFileExtension].Format(AppData.FileExtension);
public string FormattedLastPlayed => LocaleManager.Instance[LocaleKeys.GameListHeaderLastPlayed].Format(AppData.LastPlayedString);
public string FormattedPlayTime => LocaleManager.Instance[LocaleKeys.GameListHeaderTimePlayed].Format(AppData.TimePlayedString);
public string FormattedFileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize].Format(AppData.FileSizeString);
public string FormattedLdnInfo =>
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderHostedGames].Format(AppData.GameCount)}" +
$"\n" +
$"{LocaleManager.Instance[LocaleKeys.GameListHeaderPlayerCount].Format(AppData.PlayerCount)}";
}
}

View File

@@ -14,7 +14,9 @@ namespace Ryujinx.Ava.UI.ViewModels
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
{
_dlcs = appLibrary.FindDlcsFor(titleId)
_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

@@ -69,7 +69,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadDownloadableContents()
{
(DownloadableContentModel Dlc, bool IsEnabled)[] dlcs = _applicationLibrary.FindDlcConfigurationFor(_applicationData.Id);
IEnumerable<(DownloadableContentModel Dlc, bool IsEnabled)> dlcs = _applicationLibrary.DownloadableContents.Items
.Where(it => it.Dlc.TitleIdBase == _applicationData.IdBase);
bool hasBundledContent = false;
foreach ((DownloadableContentModel dlc, bool isEnabled) in dlcs)

View File

@@ -18,6 +18,34 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
[ObservableProperty] private double _gyroDeadzone;
[ObservableProperty] private bool _enableCemuHookMotion;
private bool _enableCemuHookMotion;
public bool EnableCemuHookMotion
{
get => _enableCemuHookMotion;
set
{
if (value)
{
EnableHandheldMotion = false;
}
_enableCemuHookMotion = value;
OnPropertyChanged();
}
}
private bool _enableHandheldMotion;
public bool EnableHandheldMotion
{
get => _enableHandheldMotion;
set
{
if (value)
{
EnableCemuHookMotion = false;
}
_enableHandheldMotion = value;
OnPropertyChanged();
}
}
}
}

View File

@@ -349,9 +349,16 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool HasCompatibilityEntry => SelectedApplication.HasPlayabilityInfo;
public bool HasCompatibilityEntry
{
get
{
DynamicData.Kernel.Optional<ApplicationData> appData =
ApplicationLibrary.Applications.Lookup(SelectedApplication.Id);
public bool HasDlc => ApplicationLibrary.HasDlcs(SelectedApplication.Id);
return appData.HasValue && appData.Value.HasPlayabilityInfo;
}
}
public bool OpenUserSaveDirectoryEnabled => SelectedApplication.HasControlHolder && SelectedApplication.ControlHolder.Value.UserAccountSaveDataSize > 0;

View File

@@ -41,7 +41,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private void LoadUpdates()
{
(TitleUpdateModel TitleUpdate, bool IsSelected)[] updates = ApplicationLibrary.FindUpdateConfigurationFor(ApplicationData.Id);
IEnumerable<(TitleUpdateModel TitleUpdate, bool IsSelected)> updates = ApplicationLibrary.TitleUpdates.Items
.Where(it => it.TitleUpdate.TitleIdBase == ApplicationData.IdBase);
bool hasBundledContent = false;
SelectedUpdate = new TitleUpdateViewModelNoUpdate();

View File

@@ -61,6 +61,17 @@
Margin="5, 0"
Text="{Binding GyroDeadzone, StringFormat=\{0:0.00\}}" />
</StackPanel>
<Separator
Height="1"
Margin="0,5" />
<CheckBox
Margin="5"
IsChecked="{Binding EnableHandheldMotion}">
<TextBlock
Margin="0,3,0,0"
VerticalAlignment="Center"
Text="{ext:Locale ControllerSettingsMotionUseHandheldCompatibleMotion}" />
</CheckBox>
<Separator
Height="1"
Margin="0,5" />

View File

@@ -30,6 +30,7 @@ namespace Ryujinx.Ava.UI.Views.Input
Sensitivity = config.Sensitivity,
GyroDeadzone = config.GyroDeadzone,
EnableCemuHookMotion = config.EnableCemuHookMotion,
EnableHandheldMotion = config.EnableHandheldMotion,
};
InitializeComponent();
@@ -58,6 +59,7 @@ namespace Ryujinx.Ava.UI.Views.Input
config.DsuServerHost = content._viewModel.DsuServerHost;
config.DsuServerPort = content._viewModel.DsuServerPort;
config.EnableCemuHookMotion = content._viewModel.EnableCemuHookMotion;
config.EnableHandheldMotion = content._viewModel.EnableHandheldMotion;
config.MirrorInput = content._viewModel.MirrorInput;
};

View File

@@ -29,6 +29,7 @@ using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL2;
using Ryujinx.Input.SDL3;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -107,7 +108,7 @@ namespace Ryujinx.Ava.UI.Windows
if (Program.PreviewerDetached)
{
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver());
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL2GamepadDriver(), new SDL3MotionDriver());
_ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it);
this.ScalingChanged += OnScalingChanged;

View File

@@ -49,9 +49,6 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public int PlayerCount { get; set; }
public int GameCount { get; set; }
public bool HasLdnGames => PlayerCount != 0 && GameCount != 0;
public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }

View File

@@ -128,16 +128,11 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
DynamicData.Kernel.Optional<ApplicationData> appData = Applications.Lookup(id);
if (appData.HasValue)
return appData.Value.Name;
if (!DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
return id.ToString("X16");
string name = Path.GetFileNameWithoutExtension(dlcData.FileName)!;
int idx = name.IndexOf('[');
if (idx != -1)
name = name[..idx];
if (DownloadableContents.Keys.FindFirst(x => x.TitleId == id).TryGet(out DownloadableContentModel dlcData))
return Path.GetFileNameWithoutExtension(dlcData.FileName);
return name;
return id.ToString("X16");
}
public bool FindApplication(ulong id, out ApplicationData foundData)
@@ -147,30 +142,6 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
return appData.HasValue;
}
public bool FindUpdate(ulong id, out TitleUpdateModel foundData)
{
Gommon.Optional<TitleUpdateModel> appData =
TitleUpdates.Keys.FindFirst(x => x.TitleId == id);
foundData = appData.HasValue ? appData.Value : null;
return appData.HasValue;
}
public TitleUpdateModel[] FindUpdatesFor(ulong id)
=> TitleUpdates.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (TitleUpdateModel TitleUpdate, bool IsSelected)[] FindUpdateConfigurationFor(ulong id)
=> TitleUpdates.Items.Where(x => x.TitleUpdate.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public DownloadableContentModel[] FindDlcsFor(ulong id)
=> DownloadableContents.Keys.Where(x => x.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public (DownloadableContentModel Dlc, bool IsEnabled)[] FindDlcConfigurationFor(ulong id)
=> DownloadableContents.Items.Where(x => x.Dlc.TitleIdBase == (id & ~0x1FFFUL)).ToArray();
public bool HasDlcs(ulong id)
=> DownloadableContents.Keys.Any(x => x.TitleIdBase == (id & ~0x1FFFUL));
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
/// <exception cref="InvalidDataException">The NCA header could not be decrypted.</exception>