From b5c82f8a5b3aa9aee492861d76c63d79127ceecc Mon Sep 17 00:00:00 2001 From: Mahmoud Al-Qudsi Date: Tue, 10 Jun 2025 13:49:18 -0500 Subject: [PATCH 1/6] Exclude time spent with emulator paused from play time With this change, the play time reported internally by a game's gameplay timer should match (or be much closer to matching) what Ryujinx displays in the application library. Aside from this being closer to the natural expectation of what "hours played" would take into account (as by definition time spent paused is time spent not playing), this also brings us closer to the behavior of other emulators and game libraries. --- src/Ryujinx/Systems/AppHost.cs | 7 ++++++- src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs | 7 +++++-- src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 74005400c..94c2f2900 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -75,6 +75,7 @@ namespace Ryujinx.Ava.Systems private readonly long _ticksPerFrame; private readonly Stopwatch _chrono; + private readonly Stopwatch _pauseTimer; private long _ticks; private readonly AccountManager _accountManager; @@ -175,6 +176,7 @@ namespace Ryujinx.Ava.Systems _chrono = new Stopwatch(); _ticksPerFrame = Stopwatch.Frequency / TargetFps; + _pauseTimer = new Stopwatch(); if (ApplicationPath.StartsWith("@SystemContent")) { @@ -616,7 +618,7 @@ namespace Ryujinx.Ava.Systems private void Dispose() { if (Device.Processes != null) - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText); + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _pauseTimer.Elapsed); ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; @@ -635,6 +637,7 @@ namespace Ryujinx.Ava.Systems _gpuCancellationTokenSource.Dispose(); _chrono.Stop(); + _pauseTimer.Stop(); } public void DisposeGpu() @@ -877,6 +880,7 @@ namespace Ryujinx.Ava.Systems Device?.System.TogglePauseEmulation(false); _viewModel.IsPaused = false; + _pauseTimer.Stop(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); } @@ -886,6 +890,7 @@ namespace Ryujinx.Ava.Systems Device?.System.TogglePauseEmulation(true); _viewModel.IsPaused = true; + _pauseTimer.Start(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); } diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs index 9d8488aeb..e80fae009 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Systems.AppLibrary @@ -33,7 +34,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary /// /// Updates and . Call this after a game ends. /// - public void UpdatePostGame() + /// The amount of time emulation was paused while playing. + public void UpdatePostGame(TimeSpan pauseTime) { DateTime? prevLastPlayed = LastPlayed; UpdatePreGame(); @@ -44,7 +46,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary } TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value; - double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds; + Debug.Assert(pauseTime <= diff); + double newTotalSeconds = TimePlayed.Add(diff - pauseTime).TotalSeconds; TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero)); } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 8b9b04511..1ea9d009d 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1688,8 +1688,8 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId) - => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame()); + public static void UpdateGameMetadata(string titleId, TimeSpan pauseTime) + => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(pauseTime)); public void RefreshFirmwareStatus() { From 32ae313d779c33ac522cf69e76ff479070de5512 Mon Sep 17 00:00:00 2001 From: Mahmoud Al-Qudsi Date: Wed, 11 Jun 2025 15:37:56 -0500 Subject: [PATCH 2/6] Rework updates of application metadata gameplay hours Rely on `AppHost` to more accurately track active gameplay time instead of doing naive DateTime.UtcNow subtraction in UpdatePre/UpdatePostGame() calls. --- src/Ryujinx/Systems/AppHost.cs | 14 ++++++++------ .../Systems/AppLibrary/ApplicationMetadata.cs | 17 +++-------------- .../UI/ViewModels/MainWindowViewModel.cs | 4 ++-- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 94c2f2900..1c5f64309 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -75,7 +75,7 @@ namespace Ryujinx.Ava.Systems private readonly long _ticksPerFrame; private readonly Stopwatch _chrono; - private readonly Stopwatch _pauseTimer; + private readonly Stopwatch _playTimer; private long _ticks; private readonly AccountManager _accountManager; @@ -176,7 +176,7 @@ namespace Ryujinx.Ava.Systems _chrono = new Stopwatch(); _ticksPerFrame = Stopwatch.Frequency / TargetFps; - _pauseTimer = new Stopwatch(); + _playTimer = new Stopwatch(); if (ApplicationPath.StartsWith("@SystemContent")) { @@ -567,6 +567,7 @@ namespace Ryujinx.Ava.Systems public void Stop() { _isActive = false; + _playTimer.Stop(); } private void Exit() @@ -618,7 +619,7 @@ namespace Ryujinx.Ava.Systems private void Dispose() { if (Device.Processes != null) - MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _pauseTimer.Elapsed); + MainWindowViewModel.UpdateGameMetadata(Device.Processes.ActiveApplication.ProgramIdText, _playTimer.Elapsed); ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; @@ -637,7 +638,7 @@ namespace Ryujinx.Ava.Systems _gpuCancellationTokenSource.Dispose(); _chrono.Stop(); - _pauseTimer.Stop(); + _playTimer.Stop(); } public void DisposeGpu() @@ -871,6 +872,7 @@ namespace Ryujinx.Ava.Systems ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); + _playTimer.Start(); return true; } @@ -880,7 +882,7 @@ namespace Ryujinx.Ava.Systems Device?.System.TogglePauseEmulation(false); _viewModel.IsPaused = false; - _pauseTimer.Stop(); + _playTimer.Start(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI); Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed"); } @@ -890,7 +892,7 @@ namespace Ryujinx.Ava.Systems Device?.System.TogglePauseEmulation(true); _viewModel.IsPaused = true; - _pauseTimer.Start(); + _playTimer.Stop(); _viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]); Logger.Info?.Print(LogClass.Emulation, "Emulation was paused"); } diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs index e80fae009..8940657b0 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationMetadata.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Text.Json.Serialization; namespace Ryujinx.Ava.Systems.AppLibrary @@ -34,21 +33,11 @@ namespace Ryujinx.Ava.Systems.AppLibrary /// /// Updates and . Call this after a game ends. /// - /// The amount of time emulation was paused while playing. - public void UpdatePostGame(TimeSpan pauseTime) + /// The active gameplay time this past session. + public void UpdatePostGame(TimeSpan playTime) { - DateTime? prevLastPlayed = LastPlayed; UpdatePreGame(); - - if (!prevLastPlayed.HasValue) - { - return; - } - - TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value; - Debug.Assert(pauseTime <= diff); - double newTotalSeconds = TimePlayed.Add(diff - pauseTime).TotalSeconds; - TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero)); + TimePlayed += playTime; } } } diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 1ea9d009d..5e7df4d62 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1688,8 +1688,8 @@ namespace Ryujinx.Ava.UI.ViewModels RendererHostControl.Focus(); }); - public static void UpdateGameMetadata(string titleId, TimeSpan pauseTime) - => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(pauseTime)); + public static void UpdateGameMetadata(string titleId, TimeSpan playTime) + => ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => appMetadata.UpdatePostGame(playTime)); public void RefreshFirmwareStatus() { From 557c2a50b2164823d2731f9ddd8e57ae27f01486 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 16 Jun 2025 02:04:48 -0500 Subject: [PATCH 3/6] infra: Add NuGet config to solution items --- Ryujinx.sln | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Ryujinx.sln b/Ryujinx.sln index 9ed282d09..4babf3fb9 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -77,6 +77,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Gene 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.BuildValidationTasks", "src\Ryujinx.BuildValidationTasks\Ryujinx.BuildValidationTasks.csproj", "{4A89A234-4F19-497D-A576-DDE8CDFC5B22}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig @@ -84,10 +86,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\workflows\canary.yml = .github\workflows\canary.yml Directory.Packages.props = Directory.Packages.props .github\workflows\release.yml = .github\workflows\release.yml + nuget.config = nuget.config EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.BuildValidationTasks", "src\Ryujinx.BuildValidationTasks\Ryujinx.BuildValidationTasks.csproj", "{4A89A234-4F19-497D-A576-DDE8CDFC5B22}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 6803c91da8784e56284e5740150e9df515da0d2a Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 16 Jun 2025 02:05:11 -0500 Subject: [PATCH 4/6] infra: Add package source mappings for Ryujinx.UpdateClient to silence compile warnings --- nuget.config | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nuget.config b/nuget.config index 4e51027dc..77eadcf88 100644 --- a/nuget.config +++ b/nuget.config @@ -8,4 +8,16 @@ + + + + + + + + + + From 973c6ba5df30d25f79097c6708b29bdc4f371bef Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 16 Jun 2025 02:06:45 -0500 Subject: [PATCH 5/6] UI: RPC: Squeakross: Home Squeak Home image docs: compat: Squeakross: Home Squeak Home: Playable --- docs/compatibility.csv | 1 + src/Ryujinx.Common/TitleIDs.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/compatibility.csv b/docs/compatibility.csv index 684fc7833..56d737aa2 100644 --- a/docs/compatibility.csv +++ b/docs/compatibility.csv @@ -2746,6 +2746,7 @@ 01005D701264A000,"SpyHack",,playable,2021-04-15 10:53:51 010077B00E046000,"Spyro™ Reignited Trilogy",nvdec;UE4,playable,2022-09-11 18:38:33 0100085012A0E000,"Squeakers",,playable,2020-12-13 12:13:05 +0100E1D01EB2E000,"Squeakross: Home Squeak Home",,playable,2025-06-16 02:02:00 010009300D31C000,"Squidgies Takeover",,playable,2020-07-20 22:28:08 0100FCD0102EC000,"Squidlit",,playable,2020-08-06 12:38:32 0100EBF00E702000,"STAR OCEAN First Departure R",nvdec,playable,2021-07-05 19:29:16 diff --git a/src/Ryujinx.Common/TitleIDs.cs b/src/Ryujinx.Common/TitleIDs.cs index 16c9ea05f..d753caa33 100644 --- a/src/Ryujinx.Common/TitleIDs.cs +++ b/src/Ryujinx.Common/TitleIDs.cs @@ -195,6 +195,7 @@ namespace Ryujinx.Common "01008d100d43e000", // Saints Row IV "0100de600beee000", // Saints Row: The Third - The Full Package "01001180021fa000", // Shovel Knight: Specter of Torment + "0100e1D01eb2e000", // Squeakross: Home Squeak Home "0100e65002bb8000", // Stardew Valley "0100d7a01b7a2000", // Star Wars: Bounty Hunter "0100800015926000", // Suika Game From 39944b20630399e303a1652ae309df7a75f02ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=E8=8C=B6=E3=82=93?= Date: Tue, 17 Jun 2025 03:21:30 -0500 Subject: [PATCH 6/6] Update Korean translation (ryubing/ryujinx!64) See merge request ryubing/ryujinx!64 --- assets/locales.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/locales.json b/assets/locales.json index a53c0047a..1158fd5a0 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -4009,7 +4009,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "Ryujinx 1.1.1403을 연상시키는 이전 Avalonia Ryujinx UI를 표시합니다. 이 기능은 Windows가 아닌 플랫폼에서는 기본적으로 활성화됩니다.\n 클래식 스타일의 타이틀 바가 돌아왔고 주요 창 레이아웃 재작업이 역전되었습니다. 이 툴팁 위의 설정 탐색 배치와 같은 작업입니다.", + "ko_KR": "Ryujinx 1.1.1403을 연상시키는 이전 Avalonia Ryujinx UI를 표시합니다. 이 기능은 윈도가 아닌 플랫폼에서는 기본적으로 활성화됩니다.\n 클래식 스타일의 타이틀 바가 돌아왔고 주요 창 레이아웃 변경 사항이 원래대로 적용됩니다. 이 툴팁 위의 설정 탐색 배치와 같은 경우입니다.", "no_NO": "Vis det eldre Avalonia Ryujinx-grensesnittet som minner om Ryujinx 1.1.1403. Dette er aktivert som standard på plattformer som ikke er Windows.\nTittellinjen i klassisk stil er tilbake, og store omarbeidinger av vindusoppsettet er reversert, for eksempel plasseringen av innstillingsnavigasjonen over dette verktøytipset.", "pl_PL": "", "pt_BR": "Mostrar a Interface Avalonia antiga do Ryujinx 1.1.1403. Esta versão é ativada por padrão nas plataformas que não sejam Windows. \nO estilo clássico da Barra de Título retorna e grande parte das mudanças do Layout de janela são revertidas; assim como as configurações de posicionamento da navegação acima dessa descrição.",