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)); +}