Compare commits

...

37 Commits

Author SHA1 Message Date
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
cc95e80ee9 misc: chore: Move converters into a directory in Helpers. Namespace unchanged 2025-01-10 20:24:53 -06:00
d75ce52bd4 UI: Show play time in one time unit, maxing out at hours. 2025-01-10 20:23:47 -06:00
4a4ea557de UI: compat: show last updated date on entry hover 2025-01-10 01:43:34 -06:00
33f42adb11 Merge remote-tracking branch 'origin/master' 2025-01-09 22:09:01 -06:00
cca429d46a misc: chore: restore not enable 2025-01-09 21:42:54 -06:00
57 changed files with 4055 additions and 4190 deletions

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" />
@ -42,17 +42,17 @@
<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" />
<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

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

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

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

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

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

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

@ -897,7 +897,7 @@ namespace Ryujinx.Ava
{
#pragma warning disable CA1416 // This call site is reachable on all platforms
// SelectGraphicsBackend does a check for Mac, on top of checking if it's an ARM Mac. This isn't a problem.
GraphicsBackend.Metal => new MetalRenderer((RendererHost.EmbeddedWindow as EmbeddedWindowMetal)!.CreateSurface),
GraphicsBackend.Metal => new MetalRenderer(() => new SharpMetal.QuartzCore.CAMetalLayer(((EmbeddedWindowMetal)RendererHost.EmbeddedWindow).MetalLayer)),
#pragma warning restore CA1416
GraphicsBackend.Vulkan => VulkanRenderer.Create(
ConfigurationState.Instance.Graphics.PreferredGpu,

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
using DiscordRPC;
using Gommon;
using Humanizer;
using Humanizer.Localisation;
using Ryujinx.Ava.Utilities.AppLibrary;
@ -45,16 +46,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 +67,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

View File

@ -12,8 +12,6 @@ using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Metal;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE;
using Ryujinx.Input;
@ -312,10 +310,10 @@ namespace Ryujinx.Headless
if (options.GraphicsBackend == GraphicsBackend.Metal && window is MetalWindow metalWindow && OperatingSystem.IsMacOS())
{
return new MetalRenderer(metalWindow.GetLayer);
return new Graphics.Metal.MetalRenderer(metalWindow.GetLayer);
}
return new OpenGLRenderer();
return new Graphics.OpenGL.OpenGLRenderer();
}
private static Switch InitializeEmulationContext(WindowBase window, IRenderer renderer, Options options)

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

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

@ -124,7 +124,7 @@ namespace Ryujinx.Headless
_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()
{

View File

@ -24,7 +24,7 @@ namespace Ryujinx.Headless
_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,6 +1,7 @@
using Humanizer;
using LibHac.Tools.Fs;
using Ryujinx.Ava;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
@ -137,7 +138,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 +192,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 +247,7 @@ namespace Ryujinx.Headless
protected abstract void SwapBuffers();
public abstract SDL_WindowFlags GetWindowFlags();
public abstract SDL_WindowFlags WindowFlags { get; }
private string GetGpuDriverName()
{

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

@ -73,7 +73,7 @@
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
@ -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" />

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Platform;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Configuration;
@ -28,7 +27,7 @@ namespace Ryujinx.Ava.UI.Renderer
protected nint WindowHandle { get; set; }
protected nint X11Display { get; set; }
protected nint NsView { get; set; }
protected nint MetalLayer { get; set; }
public nint MetalLayer { get; protected set; }
public delegate void UpdateBoundsCallbackDelegate(Rect rect);
private UpdateBoundsCallbackDelegate _updateBoundsCallback;

View File

@ -1,8 +1,10 @@
using SharpMetal.QuartzCore;
using System;
using System.Runtime.Versioning;
namespace Ryujinx.Ava.UI.Renderer
{
[SupportedOSPlatform("macos")]
public class EmbeddedWindowMetal : EmbeddedWindow
{
public CAMetalLayer CreateSurface()

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

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

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

@ -164,7 +164,7 @@ namespace Ryujinx.Ava.UI.Windows
});
}
private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e)
private void ApplicationLibrary_LdnGameDataReceived(LdnGameDataReceivedEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
@ -408,13 +408,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()

View File

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

View File

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

View File

@ -45,7 +45,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public const string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
public Language DesiredLanguage { get; set; }
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
public event EventHandler<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
public event Action<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
public readonly IObservableCache<ApplicationData, ulong> Applications;
public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates;
@ -779,7 +779,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
using HttpClient httpClient = new HttpClient();
string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games");
ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData);
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{
LdnData = ldnGameDataArray
});
@ -787,7 +787,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.\n{ex.Message}");
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{
LdnData = Array.Empty<LdnGameData>()
});
@ -795,7 +795,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
}
else
{
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{
LdnData = Array.Empty<LdnGameData>()
});

View File

@ -1,4 +1,5 @@
using Gommon;
using Humanizer;
using nietras.SeparatedValues;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Logging;
@ -56,7 +57,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
@ -83,12 +84,14 @@ namespace Ryujinx.Ava.Utilities.Compat
public LocaleKeys? Status { 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 => Labels
.Where(it => !it.StartsWithIgnoreCase("status"))
.Select(FormatLabelName)
.JoinToString(", ");
@ -97,7 +100,9 @@ namespace Ryujinx.Ava.Utilities.Compat
StringBuilder sb = new("CompatibilityEntry: {");
sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
sb.Append($"{nameof(TitleId)}={TitleId}, ");
sb.Append($"{nameof(Labels)}=\"{Labels}\", ");
sb.Append($"{nameof(Labels)}={
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
}, ");
sb.Append($"{nameof(Status)}=\"{Status}\", ");
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
sb.Append('}');

View File

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

View File

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