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/3] 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/3] 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 b1cde5fd9745fcb3e5d82bc5343e99caa711fa1e Mon Sep 17 00:00:00 2001 From: yeager Date: Tue, 17 Jun 2025 13:05:39 -0500 Subject: [PATCH 3/3] Updated Swedish translation (ryubing/ryujinx!66) See merge request ryubing/ryujinx!66 --- assets/locales.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index 1158fd5a0..8b60c04bd 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -2089,7 +2089,7 @@ "pl_PL": "Całkowity czas gry: {0}", "pt_BR": "Tempo total de jogo: {0}", "ru_RU": "", - "sv_SE": "", + "sv_SE": "Total speltid: {0}", "th_TH": "", "tr_TR": "Toplam Oyun Süresi: {0}", "uk_UA": "", @@ -7214,7 +7214,7 @@ "pl_PL": "", "pt_BR": "Configuração encontrada:\n\nNome:\t{0}\nGUID:\t{1}\n\n Aguardando conexão do controle...", "ru_RU": "", - "sv_SE": "", + "sv_SE": "Konfiguration hittad:\n\nNamn:\t{0}\nGUID:\t{1}\n\n Väntar på anslutning till kontroller...", "th_TH": "", "tr_TR": "", "uk_UA": "", @@ -13139,7 +13139,7 @@ "pl_PL": "", "pt_BR": "Falha em atualizar a versão do Ryujinx recebida do servidor de atualização.", "ru_RU": "", - "sv_SE": "", + "sv_SE": "Det gick inte att konvertera Ryujinx-versionen som mottogs från uppdateringsservern.", "th_TH": "", "tr_TR": "", "uk_UA": "", @@ -16564,7 +16564,7 @@ "pl_PL": "", "pt_BR": "Se esta opção está ativada nas configurações customizadas, as configurações globais de entrada serão usadas.\n\nNas configurações globais: você pode ativar ou desativá-las se necessário; está configuração será herdada por qualquer nova configuração customizada criada.", "ru_RU": "Если эта опция включена в пользовательских настройках, будет использована глобальная конфигурация ввода.\n\nВ глобальных настройках: переключите эту опцию по своему усмотрению, это будет унаследовано для вновь созданых пользовательских конфигураций", - "sv_SE": "", + "sv_SE": "Om det här alternativet är aktiverat i anpassade inställningar kommer den globala inmatningskonfigurationen att användas.\n\nI de globala inställningarna: du kan aktivera eller inaktivera det efter behov; den här inställningen kommer att ärvas av alla nya anpassade konfigurationer som skapas.", "th_TH": "", "tr_TR": "", "uk_UA": "", @@ -23439,7 +23439,7 @@ "pl_PL": "", "pt_BR": "Ver Registro", "ru_RU": "", - "sv_SE": "", + "sv_SE": "Visa ändringslogg", "th_TH": "ด", "tr_TR": "", "uk_UA": "", @@ -24539,7 +24539,7 @@ "pl_PL": "Gry i Aplikacje", "pt_BR": "Jogos e Aplicativos", "ru_RU": "Игры и Приложения", - "sv_SE": "Spel och Applikationer", + "sv_SE": "Spel och applikationer", "th_TH": "", "tr_TR": "Oyunlar ve Uygulamalar", "uk_UA": "Ігри та Додатки", @@ -24589,7 +24589,7 @@ "pl_PL": "Problemy i Cechy", "pt_BR": "Problemas e Características", "ru_RU": "Проблемы и Особенности", - "sv_SE": "Problem och Egenskaper", + "sv_SE": "Problem och egenskaper", "th_TH": "", "tr_TR": "Sorunlar ve Özellikler", "uk_UA": "Проблеми та Особливості",