Merge branch 'master' into SDL3
# Conflicts: # src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs
This commit is contained in:
commit
106b37f91f
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -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: |
|
||||
|
4
.github/workflows/canary.yml
vendored
4
.github/workflows/canary.yml
vendored
@ -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: |
|
||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -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: |
|
||||
|
@ -42,7 +42,7 @@
|
||||
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
|
||||
<PackageVersion Include="Gommon" Version="2.7.0.1" />
|
||||
<PackageVersion Include="Gommon" Version="2.7.0.2" />
|
||||
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
||||
<PackageVersion Include="Sep" Version="0.6.0" />
|
||||
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -284,7 +284,7 @@ namespace Ryujinx.HLE.HOS
|
||||
ProcessCreationInfo creationInfo = new("Service", 1, 0, 0x8000000, 1, Flags, 0, 0);
|
||||
|
||||
uint[] defaultCapabilities = {
|
||||
0x030363F7,
|
||||
(((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u,
|
||||
0x1FFFFFCF,
|
||||
0x207FFFEF,
|
||||
0x47E0060F,
|
||||
|
@ -63,6 +63,7 @@ namespace Ryujinx.HLE.HOS.Kernel
|
||||
TickSource = tickSource;
|
||||
Device = device;
|
||||
Memory = memory;
|
||||
KScheduler.CpuCoresCount = device.CpuCoresCount;
|
||||
|
||||
Running = true;
|
||||
|
||||
|
@ -37,7 +37,7 @@ namespace Ryujinx.HLE.HOS.Kernel
|
||||
return result;
|
||||
}
|
||||
|
||||
process.DefaultCpuCore = 3;
|
||||
process.DefaultCpuCore = KScheduler.CpuCoresCount - 1;
|
||||
|
||||
context.Processes.TryAdd(process.Pid, process);
|
||||
|
||||
|
@ -277,7 +277,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
return result;
|
||||
}
|
||||
|
||||
result = Capabilities.InitializeForUser(capabilities, MemoryManager);
|
||||
result = Capabilities.InitializeForUser(capabilities, MemoryManager, IsApplication);
|
||||
|
||||
if (result != Result.Success)
|
||||
{
|
||||
|
@ -35,15 +35,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
DebuggingFlags &= ~3u;
|
||||
KernelReleaseVersion = KProcess.KernelVersionPacked;
|
||||
|
||||
return Parse(capabilities, memoryManager);
|
||||
return Parse(capabilities, memoryManager, false);
|
||||
}
|
||||
|
||||
public Result InitializeForUser(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager)
|
||||
public Result InitializeForUser(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager, bool isApplication)
|
||||
{
|
||||
return Parse(capabilities, memoryManager);
|
||||
return Parse(capabilities, memoryManager, isApplication);
|
||||
}
|
||||
|
||||
private Result Parse(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager)
|
||||
private Result Parse(ReadOnlySpan<uint> capabilities, KPageTableBase memoryManager, bool isApplication)
|
||||
{
|
||||
int mask0 = 0;
|
||||
int mask1 = 0;
|
||||
@ -54,7 +54,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
|
||||
if (cap.GetCapabilityType() != CapabilityType.MapRange)
|
||||
{
|
||||
Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager);
|
||||
Result result = ParseCapability(cap, ref mask0, ref mask1, memoryManager, isApplication);
|
||||
|
||||
if (result != Result.Success)
|
||||
{
|
||||
@ -120,7 +120,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager)
|
||||
private Result ParseCapability(uint cap, ref int mask0, ref int mask1, KPageTableBase memoryManager, bool isApplication)
|
||||
{
|
||||
CapabilityType code = cap.GetCapabilityType();
|
||||
|
||||
@ -176,6 +176,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
|
||||
AllowedCpuCoresMask = GetMaskFromMinMax(lowestCpuCore, highestCpuCore);
|
||||
AllowedThreadPriosMask = GetMaskFromMinMax(lowestThreadPrio, highestThreadPrio);
|
||||
|
||||
if (isApplication && lowestCpuCore == 0 && highestCpuCore != 2)
|
||||
Ryujinx.Common.Logging.Logger.Error?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}! Report this to @LotP on the Ryujinx/Ryubing discord server (discord.gg/ryujinx)!");
|
||||
else if (isApplication)
|
||||
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -2683,7 +2683,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall
|
||||
return KernelResult.InvalidCombination;
|
||||
}
|
||||
|
||||
if ((uint)preferredCore > 3)
|
||||
if ((uint)preferredCore > KScheduler.CpuCoresCount - 1)
|
||||
{
|
||||
if ((preferredCore | 2) != -1)
|
||||
{
|
||||
|
@ -9,13 +9,11 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
||||
partial class KScheduler : IDisposable
|
||||
{
|
||||
public const int PrioritiesCount = 64;
|
||||
public const int CpuCoresCount = 4;
|
||||
public static int CpuCoresCount;
|
||||
|
||||
private const int RoundRobinTimeQuantumMs = 10;
|
||||
|
||||
private static readonly int[] _preemptionPriorities = { 59, 59, 59, 63 };
|
||||
|
||||
private static readonly int[] _srcCoresHighestPrioThreads = new int[CpuCoresCount];
|
||||
private static int[] _srcCoresHighestPrioThreads;
|
||||
|
||||
private readonly KernelContext _context;
|
||||
private readonly int _coreId;
|
||||
@ -47,6 +45,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
||||
_coreId = coreId;
|
||||
|
||||
_currentThread = null;
|
||||
|
||||
if (_srcCoresHighestPrioThreads == null)
|
||||
{
|
||||
_srcCoresHighestPrioThreads = new int[CpuCoresCount];
|
||||
}
|
||||
}
|
||||
|
||||
private static int PreemptionPriorities(int index)
|
||||
{
|
||||
return index == CpuCoresCount - 1 ? 63 : 59;
|
||||
}
|
||||
|
||||
public static ulong SelectThreads(KernelContext context)
|
||||
@ -437,7 +445,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
|
||||
|
||||
for (int core = 0; core < CpuCoresCount; core++)
|
||||
{
|
||||
RotateScheduledQueue(context, core, _preemptionPriorities[core]);
|
||||
RotateScheduledQueue(context, core, PreemptionPriorities(core));
|
||||
}
|
||||
|
||||
context.CriticalSection.Leave();
|
||||
|
@ -24,14 +24,14 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
// not large enough.
|
||||
private const int PointerBufferSize = 0x8000;
|
||||
|
||||
private readonly static uint[] _defaultCapabilities = {
|
||||
0x030363F7,
|
||||
private static uint[] _defaultCapabilities => [
|
||||
(((uint)KScheduler.CpuCoresCount - 1) << 24) + (((uint)KScheduler.CpuCoresCount - 1) << 16) + 0x63F7u,
|
||||
0x1FFFFFCF,
|
||||
0x207FFFEF,
|
||||
0x47E0060F,
|
||||
0x0048BFFF,
|
||||
0x01007FFF,
|
||||
};
|
||||
];
|
||||
|
||||
// The amount of time Dispose() will wait to Join() the thread executing the ServerLoop()
|
||||
private static readonly TimeSpan _threadJoinTimeout = TimeSpan.FromSeconds(3);
|
||||
|
@ -32,6 +32,8 @@ namespace Ryujinx.HLE
|
||||
public TamperMachine TamperMachine { get; }
|
||||
public IHostUIHandler UIHandler { get; }
|
||||
|
||||
public int CpuCoresCount = 4; //Switch 1 has 4 cores
|
||||
|
||||
public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch;
|
||||
public bool CustomVSyncIntervalEnabled { get; set; } = false;
|
||||
public int CustomVSyncInterval { get; set; }
|
||||
|
@ -25,7 +25,7 @@ namespace Ryujinx.Input.SDL2
|
||||
public const string Id = "JoyConPair";
|
||||
string IGamepad.Id => Id;
|
||||
|
||||
public string Name => "Nintendo Switch Joy-Con (L/R)";
|
||||
public string Name => "* Nintendo Switch Joy-Con (L/R)";
|
||||
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
|
||||
|
||||
public void Dispose()
|
||||
|
@ -1243,7 +1243,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "问答与指南",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -4018,7 +4018,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "与 PC 日期和时间重新同步",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -14193,7 +14193,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Ryujinx — це емулятор для Nintendo Switch™.\nОтримуйте всі останні новини в нашому Discord.\nРозробники, які хочуть зробити внесок, можуть дізнатися більше на нашому GitHub або в Discord.",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "Ryujinx 是一个 Nintendo Switch™ 模拟器。\n有兴趣做出贡献的开发者可以在我们的 GitHub 或 Discord 上了解更多信息。\n",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -17818,7 +17818,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "档案对话框",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -19543,7 +19543,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Необрізаних {0} тайтл(ів)...",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "正在精简 {0} 个游戏",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -19718,7 +19718,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Заголовок",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "标题",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -19743,7 +19743,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Економія місця",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "节省空间",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -19793,7 +19793,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Зшивання",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "取消精简",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -22597,6 +22597,31 @@
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListLastUpdated",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Last updated: {0}",
|
||||
"es_ES": "",
|
||||
"fr_FR": "",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "最后更新于: {0}",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "CompatibilityListWarning",
|
||||
"Translations": {
|
||||
@ -22618,7 +22643,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "此兼容性列表可能包含过时的条目。\n不要只测试 \"进入游戏\" 状态的游戏。",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -22643,7 +22668,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "正在搜索兼容性条目...",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -22668,7 +22693,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "打开兼容性列表",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -22693,7 +22718,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "仅显示拥有的游戏",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -22718,7 +22743,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "可游玩",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -22743,7 +22768,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "进入游戏",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -22768,7 +22793,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "菜单",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -22793,7 +22818,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "启动",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
@ -22818,7 +22843,7 @@
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "",
|
||||
"zh_CN": "什么都没有",
|
||||
"zh_TW": ""
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using DiscordRPC;
|
||||
using DiscordRPC;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Audio.Backends.SDL2;
|
||||
using Ryujinx.Ava;
|
||||
|
@ -133,12 +133,13 @@
|
||||
Spacing="5">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding TimePlayedString}"
|
||||
Text="{Binding LastPlayedString}"
|
||||
TextAlignment="End"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding LastPlayedString}"
|
||||
Text="{Binding TimePlayedString}"
|
||||
IsVisible="{Binding HasPlayedPreviously}"
|
||||
TextAlignment="End"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
|
@ -12,8 +12,8 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
private static readonly Lazy<PlayabilityStatusConverter> _shared = new(() => new());
|
||||
public static PlayabilityStatusConverter Shared => _shared.Value;
|
||||
|
||||
public object Convert(object? value, Type _, object? __, CultureInfo ___) =>
|
||||
value.Cast<LocaleKeys>() switch
|
||||
public object Convert(object value, Type _, object __, CultureInfo ___)
|
||||
=> value.Cast<LocaleKeys>() switch
|
||||
{
|
||||
LocaleKeys.CompatibilityListNothing or
|
||||
LocaleKeys.CompatibilityListBoots or
|
||||
@ -22,7 +22,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
_ => Brushes.ForestGreen
|
||||
};
|
||||
|
||||
public object ConvertBack(object? value, Type _, object? __, CultureInfo ___)
|
||||
public object ConvertBack(object value, Type _, object __, CultureInfo ___)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,15 @@
|
||||
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
|
||||
{
|
||||
get => _isLeft;
|
||||
@ -32,7 +22,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
}
|
||||
|
||||
private bool _isRight;
|
||||
|
||||
public bool IsRight
|
||||
{
|
||||
get => _isRight;
|
||||
@ -46,43 +35,13 @@ 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;
|
||||
|
||||
private string _leftStickPosition;
|
||||
[ObservableProperty] private string _leftStickPosition;
|
||||
|
||||
public string LeftStickPosition
|
||||
{
|
||||
get => _leftStickPosition;
|
||||
set
|
||||
{
|
||||
_leftStickPosition = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _rightStickPosition;
|
||||
|
||||
public string RightStickPosition
|
||||
{
|
||||
get => _rightStickPosition;
|
||||
set
|
||||
{
|
||||
_rightStickPosition = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
[ObservableProperty] private string _rightStickPosition;
|
||||
|
||||
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -741,7 +741,10 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
Applications.ToObservableChangeSet()
|
||||
.Filter(Filter)
|
||||
.Sort(GetComparer())
|
||||
.Bind(out _appsObservableList).AsObservableList();
|
||||
#pragma warning disable MVVMTK0034
|
||||
.Bind(out _appsObservableList)
|
||||
#pragma warning restore MVVMTK0034
|
||||
.AsObservableList();
|
||||
|
||||
OnPropertyChanged(nameof(AppsObservableList));
|
||||
}
|
||||
|
@ -168,7 +168,7 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
});
|
||||
}
|
||||
|
||||
private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e)
|
||||
private void ApplicationLibrary_LdnGameDataReceived(LdnGameDataReceivedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
@ -412,13 +412,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()
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using FluentAvalonia.Core;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
@ -23,6 +25,11 @@ namespace Ryujinx.Ava.UI.Windows
|
||||
|
||||
InitializeComponent();
|
||||
Load();
|
||||
|
||||
#if DEBUG
|
||||
this.AttachDevTools(new KeyGesture(Key.F12, KeyModifiers.Alt));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
public SettingsWindow()
|
||||
|
@ -37,6 +37,8 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
|
||||
|
||||
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
|
||||
|
||||
public bool HasPlayedPreviously => TimePlayedString != string.Empty;
|
||||
|
||||
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
|
||||
|
||||
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
||||
|
@ -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>()
|
||||
});
|
||||
|
@ -32,29 +32,27 @@ namespace Ryujinx.Ava.Utilities
|
||||
|
||||
public string GetContentPath(ContentManager contentManager)
|
||||
=> (contentManager ?? _contentManager)
|
||||
.GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program);
|
||||
?.GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program);
|
||||
|
||||
public bool CanStart(ContentManager contentManager, out ApplicationData appData,
|
||||
out BlitStruct<ApplicationControlProperty> appControl)
|
||||
{
|
||||
contentManager ??= _contentManager;
|
||||
if (contentManager == null)
|
||||
{
|
||||
appData = null;
|
||||
appControl = new BlitStruct<ApplicationControlProperty>(0);
|
||||
return false;
|
||||
}
|
||||
if (contentManager == null)
|
||||
goto BadData;
|
||||
|
||||
string contentPath = GetContentPath(contentManager);
|
||||
if (string.IsNullOrEmpty(contentPath))
|
||||
goto BadData;
|
||||
|
||||
appData = new() { Name = Name, Id = ProgramId, Path = GetContentPath(contentManager) };
|
||||
|
||||
if (string.IsNullOrEmpty(appData.Path))
|
||||
{
|
||||
appControl = new BlitStruct<ApplicationControlProperty>(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
appControl = StructHelpers.CreateCustomNacpData(Name, Version);
|
||||
return true;
|
||||
|
||||
BadData:
|
||||
appData = null;
|
||||
appControl = new BlitStruct<ApplicationControlProperty>(0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +1,66 @@
|
||||
using Gommon;
|
||||
using Humanizer;
|
||||
using nietras.SeparatedValues;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
public struct ColumnIndices(Func<ReadOnlySpan<char>, int> getIndex)
|
||||
{
|
||||
public const string TitleIdCol = "\"title_id\"";
|
||||
public const string GameNameCol = "\"game_name\"";
|
||||
public const string LabelsCol = "\"labels\"";
|
||||
public const string StatusCol = "\"status\"";
|
||||
public const string LastUpdatedCol = "\"last_updated\"";
|
||||
|
||||
public readonly int TitleId = getIndex(TitleIdCol);
|
||||
public readonly int GameName = getIndex(GameNameCol);
|
||||
public readonly int Labels = getIndex(LabelsCol);
|
||||
public readonly int Status = getIndex(StatusCol);
|
||||
public readonly int LastUpdated = getIndex(LastUpdatedCol);
|
||||
}
|
||||
|
||||
public class CompatibilityCsv
|
||||
{
|
||||
public static CompatibilityCsv Shared { get; set; }
|
||||
|
||||
public CompatibilityCsv(SepReader reader)
|
||||
static CompatibilityCsv()
|
||||
{
|
||||
var entries = new List<CompatibilityEntry>();
|
||||
using Stream csvStream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("RyujinxGameCompatibilityList")!;
|
||||
csvStream.Position = 0;
|
||||
|
||||
foreach (var row in reader)
|
||||
{
|
||||
entries.Add(new CompatibilityEntry(reader.Header, row));
|
||||
}
|
||||
using SepReader reader = Sep.Reader().From(csvStream);
|
||||
ColumnIndices columnIndices = new(reader.Header.IndexOf);
|
||||
|
||||
Entries = entries.Where(x => x.Status != null)
|
||||
.OrderBy(it => it.GameName).ToArray();
|
||||
Entries = reader
|
||||
.Enumerate(row => new CompatibilityEntry(ref columnIndices, row))
|
||||
.OrderBy(it => it.GameName)
|
||||
.ToArray();
|
||||
|
||||
Logger.Debug?.Print(LogClass.UI, "Compatibility CSV loaded.", "LoadCompatCsv");
|
||||
}
|
||||
|
||||
public CompatibilityEntry[] Entries { get; }
|
||||
public static CompatibilityEntry[] Entries { get; private set; }
|
||||
}
|
||||
|
||||
public class CompatibilityEntry
|
||||
{
|
||||
public CompatibilityEntry(SepReaderHeader header, SepReader.Row row)
|
||||
public CompatibilityEntry(ref ColumnIndices indices, SepReader.Row row)
|
||||
{
|
||||
if (row.ColCount != header.ColNames.Count)
|
||||
throw new InvalidDataException($"CSV row {row.RowIndex} ({row.ToString()}) has mismatched column count");
|
||||
|
||||
var titleIdRow = ColStr(row[header.IndexOf("\"title_id\"")]);
|
||||
string titleIdRow = ColStr(row[indices.TitleId]);
|
||||
TitleId = !string.IsNullOrEmpty(titleIdRow)
|
||||
? titleIdRow
|
||||
: default(Optional<string>);
|
||||
|
||||
GameName = ColStr(row[header.IndexOf("\"game_name\"")]).Trim().Trim('"');
|
||||
GameName = ColStr(row[indices.GameName]);
|
||||
|
||||
IssueLabels = ColStr(row[header.IndexOf("\"labels\"")]).Split(';');
|
||||
Status = ColStr(row[header.IndexOf("\"status\"")]).ToLower() switch
|
||||
Labels = ColStr(row[indices.Labels]).Split(';');
|
||||
Status = ColStr(row[indices.Status]).ToLower() switch
|
||||
{
|
||||
"playable" => LocaleKeys.CompatibilityListPlayable,
|
||||
"ingame" => LocaleKeys.CompatibilityListIngame,
|
||||
@ -54,8 +70,8 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (DateTime.TryParse(ColStr(row[header.IndexOf("\"last_updated\"")]), out var dt))
|
||||
LastEvent = dt;
|
||||
if (DateTime.TryParse(ColStr(row[indices.LastUpdated]), out var dt))
|
||||
LastUpdated = dt;
|
||||
|
||||
return;
|
||||
|
||||
@ -64,27 +80,31 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
|
||||
public string GameName { get; }
|
||||
public Optional<string> TitleId { get; }
|
||||
public string[] IssueLabels { get; }
|
||||
public string[] Labels { get; }
|
||||
public LocaleKeys? Status { get; }
|
||||
public DateTime LastEvent { get; }
|
||||
public DateTime LastUpdated { get; }
|
||||
|
||||
public string LocalizedLastUpdated =>
|
||||
LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
|
||||
|
||||
public string LocalizedStatus => LocaleManager.Instance[Status!.Value];
|
||||
public string FormattedTitleId => TitleId
|
||||
.OrElse(new string(' ', 16));
|
||||
|
||||
public string FormattedIssueLabels => IssueLabels
|
||||
.Where(it => !it.StartsWithIgnoreCase("status"))
|
||||
public string FormattedIssueLabels => Labels
|
||||
.Select(FormatLabelName)
|
||||
.JoinToString(", ");
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder("CompatibilityEntry: {");
|
||||
StringBuilder sb = new("CompatibilityEntry: {");
|
||||
sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
|
||||
sb.Append($"{nameof(TitleId)}={TitleId}, ");
|
||||
sb.Append($"{nameof(IssueLabels)}=\"{IssueLabels}\", ");
|
||||
sb.Append($"{nameof(Labels)}={
|
||||
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
|
||||
}, ");
|
||||
sb.Append($"{nameof(Status)}=\"{Status}\", ");
|
||||
sb.Append($"{nameof(LastEvent)}=\"{LastEvent}\"");
|
||||
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
|
||||
sb.Append('}');
|
||||
|
||||
return sb.ToString();
|
||||
@ -140,8 +160,8 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
if (value == string.Empty)
|
||||
return string.Empty;
|
||||
|
||||
var firstChar = value[0];
|
||||
var rest = value[1..];
|
||||
char firstChar = value[0];
|
||||
string rest = value[1..];
|
||||
|
||||
return $"{char.ToUpper(firstChar)}{rest}";
|
||||
}
|
||||
|
@ -44,8 +44,11 @@
|
||||
ItemsSource="{Binding CurrentEntries}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:CompatibilityEntry}">
|
||||
<Grid Width="750" ColumnDefinitions="Auto,Auto,Auto,*"
|
||||
Margin="5">
|
||||
<Grid Width="750"
|
||||
Margin="5"
|
||||
ColumnDefinitions="Auto,Auto,Auto,*"
|
||||
Background="Transparent"
|
||||
ToolTip.Tip="{Binding LocalizedLastUpdated}">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding GameName}"
|
||||
Width="320"
|
||||
|
@ -14,15 +14,6 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
public static async Task Show()
|
||||
{
|
||||
if (CompatibilityCsv.Shared is null)
|
||||
{
|
||||
await using Stream csvStream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("RyujinxGameCompatibilityList")!;
|
||||
csvStream.Position = 0;
|
||||
|
||||
CompatibilityCsv.Shared = new CompatibilityCsv(Sep.Reader().From(csvStream));
|
||||
}
|
||||
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
PrimaryButtonText = string.Empty,
|
||||
@ -51,7 +42,7 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void TextBox_OnTextChanged(object? sender, TextChangedEventArgs e)
|
||||
private void TextBox_OnTextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
if (DataContext is not CompatibilityViewModel cvm)
|
||||
return;
|
||||
|
@ -11,14 +11,13 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
[ObservableProperty] private bool _onlyShowOwnedGames = true;
|
||||
|
||||
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityCsv.Shared.Entries;
|
||||
private IEnumerable<CompatibilityEntry> _currentEntries = CompatibilityCsv.Entries;
|
||||
private readonly string[] _ownedGameTitleIds = [];
|
||||
private readonly ApplicationLibrary _appLibrary;
|
||||
|
||||
public IEnumerable<CompatibilityEntry> CurrentEntries => OnlyShowOwnedGames
|
||||
? _currentEntries.Where(x =>
|
||||
x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid))
|
||||
|| _appLibrary.Applications.Items.Any(a => a.Name.EqualsIgnoreCase(x.GameName)))
|
||||
x.TitleId.Check(tid => _ownedGameTitleIds.ContainsIgnoreCase(tid)))
|
||||
: _currentEntries;
|
||||
|
||||
public CompatibilityViewModel() {}
|
||||
@ -39,11 +38,11 @@ namespace Ryujinx.Ava.Utilities.Compat
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
SetEntries(CompatibilityCsv.Shared.Entries);
|
||||
SetEntries(CompatibilityCsv.Entries);
|
||||
return;
|
||||
}
|
||||
|
||||
SetEntries(CompatibilityCsv.Shared.Entries.Where(x =>
|
||||
SetEntries(CompatibilityCsv.Entries.Where(x =>
|
||||
x.GameName.ContainsIgnoreCase(searchTerm)
|
||||
|| x.TitleId.Check(tid => tid.ContainsIgnoreCase(searchTerm))));
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
using Humanizer;
|
||||
using Humanizer.Localisation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
@ -31,7 +33,7 @@ namespace Ryujinx.Ava.Utilities
|
||||
Gigabytes = 9,
|
||||
Terabytes = 10,
|
||||
Petabytes = 11,
|
||||
Exabytes = 12,
|
||||
Exabytes = 12
|
||||
}
|
||||
|
||||
private const double SizeBase10 = 1000;
|
||||
@ -48,22 +50,24 @@ namespace Ryujinx.Ava.Utilities
|
||||
public static string FormatTimeSpan(TimeSpan? timeSpan)
|
||||
{
|
||||
if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
|
||||
{
|
||||
// Game was never played
|
||||
return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
return string.Empty;
|
||||
|
||||
if (timeSpan.Value.TotalSeconds < 60)
|
||||
return timeSpan.Value.Humanize(1,
|
||||
countEmptyUnits: false,
|
||||
maxUnit: TimeUnit.Second,
|
||||
minUnit: TimeUnit.Second);
|
||||
|
||||
if (timeSpan.Value.TotalDays < 1)
|
||||
{
|
||||
// Game was played for less than a day
|
||||
return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// Game was played for more than a day
|
||||
TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
|
||||
string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
|
||||
|
||||
return $"{timeSpan.Value.Days}d, {onlyTimeString}";
|
||||
if (timeSpan.Value.TotalMinutes < 60)
|
||||
return timeSpan.Value.Humanize(1,
|
||||
countEmptyUnits: false,
|
||||
maxUnit: TimeUnit.Minute,
|
||||
minUnit: TimeUnit.Minute);
|
||||
|
||||
return timeSpan.Value.Humanize(1,
|
||||
countEmptyUnits: false,
|
||||
maxUnit: TimeUnit.Hour,
|
||||
minUnit: TimeUnit.Hour);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
Loading…
x
Reference in New Issue
Block a user