diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 4bad687..789150a 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -3,6 +3,9 @@ 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 +- FPS counter: the style saved in Settings now applies as soon as the session starts. Before, the counter only showed up after cycling the "FPS counter" row in the session menu. (#139) +- Back to the PC: the request to close Big Picture now only goes to Steam's own windows. Before, a full-screen game (SDL ones, like Valve's and many indies) could get it and close. (#140) ## [1.6.0-beta.7] ### Heads-up: beta version, not the stable one diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd76a5..b01c52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ 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 +- Contador de FPS: o estilo salvo em Ajustes agora vale assim que a sessão começa. Antes, o contador só aparecia depois de alternar a linha "Contador de FPS" no menu da sessão. (#139) +- Voltar ao PC: o pedido de fechar o Big Picture só vai para janelas da própria Steam. Antes, um jogo em tela cheia (os feitos com SDL, como os da Valve e muitos indies) podia receber o pedido e fechar. (#140) ## [1.6.0-beta.7] ### Atenção: versão beta, não é a estável diff --git a/docs/TESTING.md b/docs/TESTING.md index 9531d2e..99f71e3 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -238,6 +238,7 @@ Pré-condições: lançador = Steam Big Picture; Ajustes → "Mandar a Steam par - [ ] Sair pelo próprio Big Picture (Sair → Sair do Big Picture): a mesa volta e a Steam permanece na bandeja. Resultado: ______ - [ ] O processo da Steam continua no Gerenciador de Tarefas; clicar no ícone da bandeja reabre o cliente sem login nem reinício. Resultado: ______ - [ ] Com um **jogo da Steam rodando**: a Steam fica aberta e o log mostra `Steam: mantida aberta (jogo em execução, id …)`; o Big Picture ainda é fechado. Resultado: ______ +- [ ] Com um **jogo SDL em tela cheia sem bordas** aberto (ex.: CS2, Dota 2, Half-Life: Alyx ou um indie feito com SDL) e não salvo: voltar ao PC **não fecha o jogo**; o Big Picture fecha e, se o jogo passou por Big Picture na detecção, o log mostra `Fechar Big Picture: janela em tela cheia não é da Steam, mantida aberta`. Resultado: ______ - [ ] Com o ajuste **desligado**: o Big Picture fecha, a Steam fica aberta (`Steam: mantida aberta (desligado nos ajustes)`). Resultado: ______ - [ ] Desligar o ajuste, fechar e reabrir o Console Mode e iniciar uma sessão Big Picture: a preferência continua desligada e a Steam fica aberta. Resultado: ______ - [ ] Com o Steam aberto e o estado do jogo indisponível no Registro (valor `RunningAppID` ausente ou ilegível): a Steam fica aberta e o log informa que não foi possível verificar se há jogo em execução. Resultado: ______ @@ -393,3 +394,4 @@ Pré-requisito: RivaTuner Statistics Server instalado, com "Show On-Screen Displ - [ ] Com o RTSS fechado, ligar Compacto abre o RTSS e o contador aparece. - [ ] Voltar ao PC: o nosso some e o do Afterburner volta. - [ ] A escolha e o layout ficam salvos para a próxima sessão. +- [ ] Com Compacto (ou outro estilo nosso) já salvo em Ajustes, iniciar a sessão: o contador aparece e o do Afterburner some sem precisar mexer na linha do menu. diff --git a/src/ConsoleMode/Models/Models.cs b/src/ConsoleMode/Models/Models.cs index 16726e3..6fbd8de 100644 --- a/src/ConsoleMode/Models/Models.cs +++ b/src/ConsoleMode/Models/Models.cs @@ -87,6 +87,19 @@ public sealed class AppConfig /// Turning the TV on / to the PC's input when console mode starts (issue #75). public TvControlConfig Tv { get; set; } = new(); + /// + /// A copy with other monitor fields and every other setting as it is here. A copy, not a field-by-field + /// rebuild: a setting added later is carried along without anyone having to remember it. + /// + public AppConfig WithMonitors(string focusMonitor, List hideMonitors, Dictionary monitorModes) + { + var copy = (AppConfig)MemberwiseClone(); + copy.FocusMonitor = focusMonitor; + copy.HideMonitors = hideMonitors; + copy.MonitorModes = monitorModes; + return copy; + } + [JsonIgnore] public string SetupKey => $"{FocusMonitor}|{(MonitorModes.TryGetValue(FocusMonitor, out var mode) ? mode.Key : "current")}"; diff --git a/src/ConsoleMode/Services/ConsoleEngine.cs b/src/ConsoleMode/Services/ConsoleEngine.cs index f55ea61..f90e0fd 100644 --- a/src/ConsoleMode/Services/ConsoleEngine.cs +++ b/src/ConsoleMode/Services/ConsoleEngine.cs @@ -407,40 +407,7 @@ private AppConfig ResolveMonitorNames(AppConfig config) foreach (var (key, mode) in config.MonitorModes) modes[Monitors.ResolveName(key)] = mode; - return new AppConfig - { - Version = config.Version, - AppLanguage = config.AppLanguage, - FocusMonitor = focus, - HideMonitors = hide, - HideStrategy = config.HideStrategy, - FullscreenMode = config.FullscreenMode, - PlaynitePath = config.PlaynitePath, - AudioDeviceId = config.AudioDeviceId, - AudioDeviceName = config.AudioDeviceName, - AudioAutoSwitch = config.AudioAutoSwitch, - FpsLimit = config.FpsLimit, - MonitorModes = modes, - HdrEnable = config.HdrEnable, - VrrEnable = config.VrrEnable, - UiMode = config.UiMode, - TourDone = config.TourDone, - ConfirmedSetup = config.ConfirmedSetup, - CheckUpdates = config.CheckUpdates, - BetaUpdates = config.BetaUpdates, - HomeShortcut = config.HomeShortcut, - MenuShortcut = config.MenuShortcut, - ExitShortcut = config.ExitShortcut, - ShortcutsOnboardingDone = config.ShortcutsOnboardingDone, - HomeButtonShortPress = config.HomeButtonShortPress, - CloseSteamOnRestore = config.CloseSteamOnRestore, - AutoStartOnController = config.AutoStartOnController, - InterfaceSounds = config.InterfaceSounds, - ConsoleBackground = config.ConsoleBackground, - ConsoleBackgroundImage = config.ConsoleBackgroundImage, - SkippedUpdateVersion = config.SkippedUpdateVersion, - Tv = config.Tv - }; + return config.WithMonitors(focus, hide, modes); } private void MoveToFocus(string monitorName, nint[] handles, ScreenRect? rect) diff --git a/src/ConsoleMode/Services/LaunchService.cs b/src/ConsoleMode/Services/LaunchService.cs index 32f46d7..08c2213 100644 --- a/src/ConsoleMode/Services/LaunchService.cs +++ b/src/ConsoleMode/Services/LaunchService.cs @@ -136,7 +136,8 @@ public bool IsPlayniteActive(ConsoleRuntimeState state) /// /// Leaves Big Picture the clean way, through Steam's own URL (back to the desktop client), so it does not /// stay open on the desk once the screens are back. Halfway through the wait, if it is still there, the - /// window is asked to close (WM_CLOSE: the X button, not a kill). Returns whether it is gone. + /// window is asked to close (WM_CLOSE: the X button, not a kill), and only if it belongs to Steam: a game in + /// full screen can pass for Big Picture in . Returns whether it is gone. /// public bool CloseBigPicture(ConsoleRuntimeState state, int timeoutMs = 6000) { @@ -155,12 +156,27 @@ public bool CloseBigPicture(ConsoleRuntimeState state, int timeoutMs = 6000) { nudged = true; foreach (var handle in handles.Concat(GetBigPictureHandles()).Distinct()) - NativeWindows.PostMessage(handle, NativeWindows.WmClose, 0, 0); + { + if (IsSteamWindow(handle)) NativeWindows.PostMessage(handle, NativeWindows.WmClose, 0, 0); + else AppLog.Write("Fechar Big Picture: janela em tela cheia não é da Steam, mantida aberta"); + } } } return !IsBigPictureActive(state); } + private static bool IsSteamWindow(nint handle) + { + NativeWindows.GetWindowThreadProcessId(handle, out var processId); + if (processId == 0) return false; + try + { + using var process = Process.GetProcessById(unchecked((int)processId)); + return SteamShutdown.IsSteamProcess(process.ProcessName); + } + catch { return false; /* gone, or not ours to read: not known to be Steam, so left alone */ } + } + public bool IsBigPictureActive(ConsoleRuntimeState state) { if (state.CachedBigPictureHandle != 0) diff --git a/src/ConsoleMode/Services/SteamShutdown.cs b/src/ConsoleMode/Services/SteamShutdown.cs index 4faf25d..86d1b63 100644 --- a/src/ConsoleMode/Services/SteamShutdown.cs +++ b/src/ConsoleMode/Services/SteamShutdown.cs @@ -5,10 +5,17 @@ namespace ConsoleMode.Services; /// public static class SteamShutdown { + /// + /// Steam's own processes. Only their windows may be asked to close: a game is an SDL_app in full + /// screen just like Big Picture, and it must never get the WM_CLOSE meant for Steam. + /// + public static bool IsSteamProcess(string? processName) => + string.Equals(processName, "steam", StringComparison.OrdinalIgnoreCase) || + string.Equals(processName, "steamwebhelper", StringComparison.OrdinalIgnoreCase); + public static bool IsClientWindowCandidate(string processName, string title, string className, bool visible, bool hasOwner) => visible && !hasOwner && - (string.Equals(processName, "steam", StringComparison.OrdinalIgnoreCase) || - string.Equals(processName, "steamwebhelper", StringComparison.OrdinalIgnoreCase)) && + IsSteamProcess(processName) && string.Equals(title.Trim(), "Steam", StringComparison.OrdinalIgnoreCase) && (string.Equals(className, "Chrome_WidgetWin_1", StringComparison.Ordinal) || string.Equals(className, "vguiPopupWindow", StringComparison.Ordinal) || diff --git a/tests/ConsoleMode.Tests/AppConfigTests.cs b/tests/ConsoleMode.Tests/AppConfigTests.cs new file mode 100644 index 0000000..8345f17 --- /dev/null +++ b/tests/ConsoleMode.Tests/AppConfigTests.cs @@ -0,0 +1,61 @@ +using System.Reflection; +using ConsoleMode.Models; +using ConsoleMode.Services; + +namespace ConsoleMode.Tests; + +public class AppConfigTests +{ + [Fact] + public void Copy_for_a_session_swaps_only_the_monitor_fields() + { + var config = new AppConfig { FocusMonitor = "id-tv", HideMonitors = ["id-desk"] }; + var hide = new List { @"\\.\DISPLAY1" }; + var modes = new Dictionary(); + + var copy = config.WithMonitors(@"\\.\DISPLAY2", hide, modes); + + Assert.Equal(@"\\.\DISPLAY2", copy.FocusMonitor); + Assert.Same(hide, copy.HideMonitors); + Assert.Same(modes, copy.MonitorModes); + Assert.Equal("id-tv", config.FocusMonitor); + Assert.Equal(["id-desk"], config.HideMonitors); + } + + // The session used to get a field-by-field copy that forgot the FPS counter: the saved style + // was never applied when console mode started. + [Fact] + public void Copy_for_a_session_keeps_the_fps_counter() + { + var layout = new FpsOverlayLayout { SingleLine = true }; + var config = new AppConfig { FpsOverlay = RtssOverlay.Compact, FpsOverlayLayout = layout }; + + var copy = config.WithMonitors("", [], []); + + Assert.Equal(RtssOverlay.Compact, copy.FpsOverlay); + Assert.Same(layout, copy.FpsOverlayLayout); + } + + [Fact] + public void Copy_for_a_session_keeps_every_other_setting() + { + string[] swapped = [nameof(AppConfig.FocusMonitor), nameof(AppConfig.HideMonitors), nameof(AppConfig.MonitorModes), nameof(AppConfig.SetupKey)]; + var config = new AppConfig(); + var properties = typeof(AppConfig).GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanWrite && !swapped.Contains(p.Name)) + .ToList(); + foreach (var property in properties) + { + // A value that differs from the default, so a property left behind shows up. + if (property.PropertyType == typeof(string)) property.SetValue(config, "changed-" + property.Name); + else if (property.PropertyType == typeof(bool)) property.SetValue(config, !(bool)property.GetValue(config)!); + else if (property.PropertyType == typeof(int)) property.SetValue(config, (int)property.GetValue(config)! + 7); + else property.SetValue(config, Activator.CreateInstance(property.PropertyType)); + } + + var copy = config.WithMonitors("", [], []); + + foreach (var property in properties) + Assert.Equal(property.GetValue(config), property.GetValue(copy)); + } +} diff --git a/tests/ConsoleMode.Tests/SteamShutdownTests.cs b/tests/ConsoleMode.Tests/SteamShutdownTests.cs index 27150d1..379a3e7 100644 --- a/tests/ConsoleMode.Tests/SteamShutdownTests.cs +++ b/tests/ConsoleMode.Tests/SteamShutdownTests.cs @@ -17,6 +17,17 @@ public class SteamShutdownTests public void Only_unowned_visible_steam_client_windows_are_close_candidates(string process, string title, string className, bool visible, bool owned, bool expected) => Assert.Equal(expected, SteamShutdown.IsClientWindowCandidate(process, title, className, visible, owned)); + [Theory] + [InlineData("steam", true)] + [InlineData("steamwebhelper", true)] + [InlineData("SteamWebHelper", true)] + [InlineData("cs2", false)] + [InlineData("steamapp", false)] + [InlineData("", false)] + [InlineData(null, false)] + public void Only_steams_own_processes_may_be_asked_to_close(string? process, bool expected) => + Assert.Equal(expected, SteamShutdown.IsSteamProcess(process)); + [Fact] public void Steam_window_is_closed_to_tray_when_big_picture_was_the_launcher_and_no_game_is_running() {