From 296780921798a4b1951d60ce78d31935464604e9 Mon Sep 17 00:00:00 2001 From: Filipe Moreira Date: Fri, 25 Sep 2026 15:52:31 -0300 Subject: [PATCH] fix: Playnite slow start no longer flips back to the PC Playnite can swap its first big window (a loading screen on slow starts) for the main one. The exit check read the watched window disappearing as "Playnite closed" and restored the desk after a few seconds. In Playnite mode the loop now follows the new window (re-arming the exit watch) and ends the session only when Playnite.FullscreenApp exits, or stays without a big window for 20 s. The decision lives in PlayniteExitPolicy, with unit tests. Fixes #81 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.en-US.md | 4 +++ CHANGELOG.md | 4 +++ src/ConsoleMode/Models/Models.cs | 1 + src/ConsoleMode/Services/ConsoleEngine.cs | 30 +++++++++++++++++-- src/ConsoleMode/Services/LaunchService.cs | 2 ++ .../Services/PlayniteExitPolicy.cs | 19 ++++++++++++ .../ConsoleMode.Tests.csproj | 1 + .../PlayniteExitPolicyTests.cs | 24 +++++++++++++++ 8 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 src/ConsoleMode/Services/PlayniteExitPolicy.cs create mode 100644 tests/ConsoleMode.Tests/PlayniteExitPolicyTests.cs diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 726908b..554b78a 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -2,6 +2,10 @@ English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Before publishing a version, add a `## [VERSION]` section to **both** files: the workflow publishes the section matching the tag from each one and fails if either is missing. +## [Unreleased] +### Fixes +- Playnite: a slow start no longer sends you back to the PC after a few seconds. When Playnite swaps its loading window for the main one, Console Mode now follows the new window and only restores the desk once Playnite actually closes. (#81) + ## [1.5.0] ### What's new - Console interface: a full-screen, controller-first home and Settings, driven by the D-pad/stick and A/B with Xbox and PlayStation pads. Automatic mode picks it whenever a controller is connected. (#41, #42, #47) diff --git a/CHANGELOG.md b/CHANGELOG.md index e65b71f..07401b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CHANGELOG.en-US.md`. Antes de publicar uma versão, adicione uma seção `## [VERSÃO]` com o changelog daquela versão **nos dois arquivos**. O workflow publica as duas seções correspondentes à tag na mesma release e falha se faltar alguma. +## [Unreleased] +### Correções +- Playnite: quando ele demora para abrir, o app não volta mais para o PC depois de alguns segundos. Se o Playnite troca a janela de carregamento pela principal, o Console Mode passa a acompanhar a nova e só restaura a mesa quando o Playnite fecha de verdade. (#81) + ## [1.5.0] ### Novidades - Interface Console: tela inicial e Ajustes em tela cheia, pensados para o controle (direcional/analógico, A/B) com controles Xbox e PlayStation. O modo Automático escolhe essa interface sempre que há um controle conectado. (#41, #42, #47) diff --git a/src/ConsoleMode/Models/Models.cs b/src/ConsoleMode/Models/Models.cs index ab0cb09..651fae7 100644 --- a/src/ConsoleMode/Models/Models.cs +++ b/src/ConsoleMode/Models/Models.cs @@ -254,6 +254,7 @@ public sealed class ConsoleRuntimeState public string? LastAudioSwitchName { get; set; } public bool AudioPendingTarget { get; set; } public nint CachedBigPictureHandle { get; set; } + public DateTime? PlayniteWindowMissingSince { get; set; } public nint CachedXboxHandle { get; set; } public bool BigPictureWatchActive { get; set; } public bool AudioWatchComplete { get; set; } diff --git a/src/ConsoleMode/Services/ConsoleEngine.cs b/src/ConsoleMode/Services/ConsoleEngine.cs index e247252..50586e1 100644 --- a/src/ConsoleMode/Services/ConsoleEngine.cs +++ b/src/ConsoleMode/Services/ConsoleEngine.cs @@ -55,6 +55,7 @@ public void Start(AppConfig config, MonitorInfo? focusInfo, Func= 2; } + /// + /// Playnite may replace the window being watched (loading screen → main window). Follow the new + /// window while the process runs, and end the session only when it exits or stays windowless. + /// + private bool IsPlayniteExitSignaled(bool watchedGone) + { + if (watchedGone) State.CachedBigPictureHandle = 0; + var showing = Launch.IsPlayniteActive(State); + if (showing && watchedGone) + { + var handle = State.CachedBigPictureHandle; + AppLog.Write("Loop: Playnite trocou de janela; acompanhando a nova"); + OnUi(() => State.BigPictureWatchActive = NativeWindows.StartBigPictureExitWatch(handle)); + } + + if (showing) State.PlayniteWindowMissingSince = null; + else State.PlayniteWindowMissingSince ??= DateTime.Now; + var exit = PlayniteExitPolicy.IsExit(Launch.IsPlayniteRunning(), showing, State.PlayniteWindowMissingSince, DateTime.Now); + if (exit) AppLog.Write("Loop: Playnite fechou"); + return exit; + } + private bool AudioWatchNeeded() { if (!AppPaths.HasSvv || State.AudioWatchComplete) return false; diff --git a/src/ConsoleMode/Services/LaunchService.cs b/src/ConsoleMode/Services/LaunchService.cs index 561c49c..3eeca58 100644 --- a/src/ConsoleMode/Services/LaunchService.cs +++ b/src/ConsoleMode/Services/LaunchService.cs @@ -107,6 +107,8 @@ public nint[] GetPlayniteHandles() .ToArray(); } + public bool IsPlayniteRunning() => Process.GetProcessesByName("Playnite.FullscreenApp").Length > 0; + public nint[] GetFullscreenHandles(string mode) => mode == "playnite" ? GetPlayniteHandles() : GetBigPictureHandles(); diff --git a/src/ConsoleMode/Services/PlayniteExitPolicy.cs b/src/ConsoleMode/Services/PlayniteExitPolicy.cs new file mode 100644 index 0000000..24895a0 --- /dev/null +++ b/src/ConsoleMode/Services/PlayniteExitPolicy.cs @@ -0,0 +1,19 @@ +namespace ConsoleMode.Services; + +/// +/// When a Playnite session counts as over. Playnite can swap its first big window (a loading +/// screen on slow starts) for the main one, so a vanished window alone is not an exit. Pure, so it's tested. +/// +public static class PlayniteExitPolicy +{ + /// How long Playnite may run without a big window before the session ends anyway. + public static readonly TimeSpan WindowGrace = TimeSpan.FromSeconds(20); + + /// When the big window went missing, or null if it is showing. + public static bool IsExit(bool processRunning, bool bigWindowShowing, DateTime? windowMissingSince, DateTime now) + { + if (!processRunning) return true; + if (bigWindowShowing || windowMissingSince is null) return false; + return now - windowMissingSince.Value >= WindowGrace; + } +} diff --git a/tests/ConsoleMode.Tests/ConsoleMode.Tests.csproj b/tests/ConsoleMode.Tests/ConsoleMode.Tests.csproj index 14e33dd..f9122c5 100644 --- a/tests/ConsoleMode.Tests/ConsoleMode.Tests.csproj +++ b/tests/ConsoleMode.Tests/ConsoleMode.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/tests/ConsoleMode.Tests/PlayniteExitPolicyTests.cs b/tests/ConsoleMode.Tests/PlayniteExitPolicyTests.cs new file mode 100644 index 0000000..536a98c --- /dev/null +++ b/tests/ConsoleMode.Tests/PlayniteExitPolicyTests.cs @@ -0,0 +1,24 @@ +using ConsoleMode.Services; + +namespace ConsoleMode.Tests; + +public class PlayniteExitPolicyTests +{ + private static readonly DateTime T0 = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc); + + [Fact] + public void Exits_when_the_process_is_gone() => + Assert.True(PlayniteExitPolicy.IsExit(processRunning: false, bigWindowShowing: false, windowMissingSince: null, T0)); + + [Fact] + public void Stays_while_the_big_window_shows() => + Assert.False(PlayniteExitPolicy.IsExit(processRunning: true, bigWindowShowing: true, windowMissingSince: null, T0)); + + [Fact] + public void Waits_while_the_loading_window_is_swapped_for_the_main_one() => + Assert.False(PlayniteExitPolicy.IsExit(processRunning: true, bigWindowShowing: false, windowMissingSince: T0, T0 + TimeSpan.FromSeconds(5))); + + [Fact] + public void Exits_when_the_process_stays_windowless_past_the_grace() => + Assert.True(PlayniteExitPolicy.IsExit(processRunning: true, bigWindowShowing: false, windowMissingSince: T0, T0 + PlayniteExitPolicy.WindowGrace)); +}