Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
### What's new
- Session menu: while the game is open, hold Select + Y for a menu over the game with volume, resolution, audio output, FPS limit, HDR, "Back to the PC" and "Exit Console Mode". Also via `consolemode://menu`.

## [1.5.0-beta.5]
### What's new
- Console interface: list settings (launcher, audio, resolution, FPS, hide strategy, language, interface) open a picker with every option, focused on the current one; A picks, B goes back. Before, you had to keep pressing A to cycle.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
### Novidades
- Menu da sessão: com o jogo aberto, segure Select + Y para abrir um menu sobre o jogo com volume, resolução, saída de áudio, limite de FPS, HDR, "Voltar ao PC" e "Sair do Console Mode". Também por `consolemode://menu`.

## [1.5.0-beta.5]
### Novidades
- Interface Console: os ajustes de lista (abrir em, áudio, resolução, FPS, como desligar, idioma, interface) abrem um seletor com todas as opções, com foco na atual; A escolhe, B volta. Antes era preciso ir apertando A para ciclar.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Typical setup: two desk monitors and a distant HDMI TV that was off. Console Mod
- **`ConsoleMode-Portable-x64.exe`** — a single exe that keeps its data in `ConsoleMode_Data\` next to it
2. On first launch a short tour shows the screen map: pick the screen you play on (the others turn off)
3. From then on it is one click: **Play now**, or the **Console Mode 1 Click** desktop shortcut (Settings → Create shortcut). The first time with a new game screen, the TV asks "Can you see this screen?" and everything reverts on its own if nobody answers (mouse, keyboard or Xbox/PlayStation controller)
4. Or skip the PC altogether: with the app in the tray (turn on **Start with Windows**), **hold the Xbox button on the controller for 1 second** and console mode starts from the couch. **Hold Start + Select for 1 second** during a session to go back to the PC. Xbox (XInput) controllers only. In Settings you can switch to a **short press**: the app then turns off the controller shortcut for Game Bar in Windows (Win+G keeps working); if Steam is open, also turn off "Guide button focuses Steam" in Steam
4. Or skip the PC altogether: with the app in the tray (turn on **Start with Windows**), **hold the Xbox button on the controller for 1 second** and console mode starts from the couch. **Hold Start + Select for 1 second** during a session to go back to the PC. **Hold Select + Y** during a session for a menu over the game: volume, resolution, audio output, FPS limit, HDR, back to the PC or exit (not over exclusive-fullscreen games). Xbox (XInput) controllers only for the Home button. In Settings you can switch to a **short press**: the app then turns off the controller shortcut for Game Bar in Windows (Win+G keeps working); if Steam is open, also turn off "Guide button focuses Steam" in Steam
5. Automation: `consolemode://start`, `consolemode://stop` and `consolemode://show` links work from Stream Deck, launchers, scripts or any shortcut
6. Two interfaces: the **Desktop** one (mouse, screen map) and the **Console** one (full screen, big cards, D-pad/stick + A/B, works with Xbox and PlayStation pads). By default the app picks Console whenever a controller is connected; change it in Settings → Interface or with the switch button on either screen
4. When you are done, exit Big Picture / Playnite (or restore manually in Xbox mode)
Expand Down Expand Up @@ -190,7 +190,7 @@ Transforme seu PC Windows em um **console de jogos** com um clique: foque na TV,
- **`ConsoleMode-Portable-x64.exe`** — um único exe que guarda os dados em `ConsoleMode_Data\` ao lado dele
2. Na primeira vez, um tour curto mostra o mapa das telas: escolha a tela onde você joga (as outras desligam)
3. Depois é 1 clique: **Jogar agora**, ou o atalho **Console Mode 1 Click** na Área de Trabalho (Ajustes → Criar atalho). Na primeira vez com uma tela de jogo nova, a TV pergunta "Está vendo esta tela?" e tudo volta sozinho se ninguém responder (mouse, teclado ou controle de Xbox/PlayStation)
4. Ou nem encoste no PC: com o app na bandeja (ligue **Iniciar com o Windows**), **segure o botão Xbox do controle por 1 segundo** e o modo console entra direto do sofá. **Segure Start + Select por 1 segundo** durante a sessão para voltar ao PC. Só controles Xbox (XInput). Em Ajustes dá para trocar por um **toque curto**: aí o app desliga o atalho do controle para a Game Bar no Windows (Win+G continua funcionando); se a Steam estiver aberta, desligue também "Botão Guide foca a Steam" na Steam
4. Ou nem encoste no PC: com o app na bandeja (ligue **Iniciar com o Windows**), **segure o botão Xbox do controle por 1 segundo** e o modo console entra direto do sofá. **Segure Start + Select por 1 segundo** durante a sessão para voltar ao PC. **Segure Select + Y** durante a sessão para um menu sobre o jogo: volume, resolução, saída de áudio, limite de FPS, HDR, voltar ao PC ou sair (não funciona sobre jogos em tela cheia exclusiva). Só controles Xbox (XInput) para o botão Home. Em Ajustes dá para trocar por um **toque curto**: aí o app desliga o atalho do controle para a Game Bar no Windows (Win+G continua funcionando); se a Steam estiver aberta, desligue também "Botão Guide foca a Steam" na Steam
5. Automação: os links `consolemode://start`, `consolemode://stop` e `consolemode://show` funcionam em Stream Deck, launchers, scripts ou qualquer atalho
6. Duas interfaces: a **Desktop** (mouse, mapa de telas) e a **Console** (tela cheia, cartões grandes, D-pad/analógico + A/B, funciona com controles Xbox e PlayStation). Por padrão o app escolhe Console sempre que há um controle conectado; mude em Ajustes → Interface ou pelo botão de troca em qualquer uma das telas
4. Ao terminar, saia do Big Picture / Playnite (ou restaure manualmente no Modo Xbox)
Expand Down
19 changes: 19 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ Pré-condições: Ajustes → "Entrar ao conectar um controle" **ligado**; app n
- [ ] O log mostra `Controle conectado: <nome>; auto-start sim/não` a cada conexão. Resultado: ______
- [ ] Com a opção **desligada** (padrão), nada disso acontece. Resultado: ______

## 1c. Menu da sessão (Select + Y)

Pré-condições: sessão ativa com Big Picture (ou jogo borderless) na tela; controle Xbox ou PlayStation.

- [ ] Segurar **Select + Y** por ~0,3 s abre o menu centralizado na tela de jogo, por cima do Big Picture, com foco em "Voltar ao jogo". Resultado: ______
- [ ] Select + Y de novo (ou B, ou Esc) fecha o menu e o jogo continua onde estava. Resultado: ______
- [ ] Cabeçalho mostra o relógio, "Jogando há X min" e o controle detectado. Resultado: ______
- [ ] **Volume:** ◀/▶ na linha muda de 5 em 5 e o Windows reflete; A alterna mudo. O valor inicial é o atual do Windows. Resultado: ______
- [ ] **Resolução:** abre a lista com a atual marcada; escolher outra aplica na TV, o Big Picture continua aberto e a sessão não se encerra sozinha. Ao restaurar no fim, a resolução original volta. Resultado: ______
- [ ] **Saída de áudio:** lista só saídas ativas; escolher troca o som na hora e o "ao conectar" não desfaz. Resultado: ______
- [ ] **Limite de FPS** (só com RTSS): trocar o limite vale no jogo; "Sem limite" desliga o limitador; ao restaurar, o RTSS volta ao que era antes da sessão. Resultado: ______
- [ ] **HDR:** alternar liga/desliga na TV; ao restaurar, o HDR volta ao estado de antes da sessão (inclusive se você desligou um que estava ligado). Resultado: ______
- [ ] "Gravar os últimos 30 s" aparece desativado com "em breve". Resultado: ______
- [ ] **Voltar ao PC** restaura e o app fica na bandeja; **Sair do Console Mode** restaura e fecha o app. Resultado: ______
- [ ] Start + Select continua restaurando direto, sem abrir o menu. Resultado: ______
- [ ] Com **PlayStation**: D-pad/✕/○ funcionam no menu (a janela toma o primeiro plano). Resultado: ______
- [ ] Jogo em **tela cheia exclusiva**: o menu não aparece por cima (limitação documentada); Select + Y não quebra nada. Resultado: ______
- [ ] `start consolemode://menu` no cmd abre o menu; fora da sessão, não faz nada. Resultado: ______

## 2. Links `consolemode://`

- [ ] `start consolemode://start` no `cmd` com o app **fechado**: abre e entra no modo console. Resultado: ______
Expand Down
12 changes: 12 additions & 0 deletions src/ConsoleMode/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@
<Setter Property="FocusVisualSecondaryThickness" Value="0"/>
<Setter Property="FocusVisualMargin" Value="-6"/>
</Style>
<!-- Round badge for controller button hints (A, B, ☰, Y). Shared by the console screen and the session menu. -->
<Style x:Key="HintBadge" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource ControlFillColorSecondaryBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource ControlStrokeColorDefaultBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="16"/>
<Setter Property="MinWidth" Value="32"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="10,0"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>

<!-- One settings row in the console settings list: label left, value right. -->
<Style x:Key="ConsoleRow" TargetType="Button" BasedOn="{StaticResource ConsoleCard}">
<Setter Property="MinHeight" Value="76"/>
Expand Down
23 changes: 18 additions & 5 deletions src/ConsoleMode/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ public partial class App : Application
private const string ShowSignalName = @"Local\ConsoleMode.Show";
private const string StartSignalName = @"Local\ConsoleMode.Start";
private const string StopSignalName = @"Local\ConsoleMode.Stop";
private const string MenuSignalName = @"Local\ConsoleMode.Menu";

private MainWindow? _window;
private TrayService? _tray;
private ControllerHoldWatcher? _guide;
private ControllerHoldWatcher? _exitChord;
private ControllerHoldWatcher? _menuChord;
private ControllerConnectWatcher? _connect;
private Mutex? _instanceMutex;
private EventWaitHandle? _showSignal;
private EventWaitHandle? _startSignal;
private EventWaitHandle? _stopSignal;
private EventWaitHandle? _menuSignal;
private readonly List<RegisteredWaitHandle> _signalWaits = [];

public static MainWindow? MainWindowInstance { get; private set; }
Expand Down Expand Up @@ -51,14 +54,15 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args)
var protocolAction = cliArgs.Select(ProtocolService.ParseAction).FirstOrDefault(a => a is not null);
var autoStart = HasArg(ShortcutService.StartArgument) || protocolAction == ProtocolService.StartAction;
var stopRequest = protocolAction == ProtocolService.StopAction;
var menuRequest = protocolAction == ProtocolService.MenuAction;
// --tray: launched with Windows; stay in the tray until the user opens the window.
var trayOnly = !autoStart && HasArg(StartupService.TrayArgument);

_instanceMutex = new Mutex(true, InstanceMutexName, out var isFirstInstance);
if (!isFirstInstance)
{
// Hand the request to the running instance (tray) instead of fighting over the screens.
if (!trayOnly) SignalRunningInstance(autoStart ? StartSignalName : stopRequest ? StopSignalName : ShowSignalName);
if (!trayOnly) SignalRunningInstance(autoStart ? StartSignalName : stopRequest ? StopSignalName : menuRequest ? MenuSignalName : ShowSignalName);
Exit();
return;
}
Expand Down Expand Up @@ -114,6 +118,9 @@ private void ListenForSignals()
_showSignal = new EventWaitHandle(false, EventResetMode.AutoReset, ShowSignalName);
_startSignal = new EventWaitHandle(false, EventResetMode.AutoReset, StartSignalName);
_stopSignal = new EventWaitHandle(false, EventResetMode.AutoReset, StopSignalName);
_menuSignal = new EventWaitHandle(false, EventResetMode.AutoReset, MenuSignalName);
_signalWaits.Add(ThreadPool.RegisterWaitForSingleObject(_menuSignal, (_, _) =>
_window?.DispatcherQueue.TryEnqueue(() => ViewModel?.ToggleSessionMenu()), null, Timeout.Infinite, executeOnlyOnce: false));

_signalWaits.Add(ThreadPool.RegisterWaitForSingleObject(_showSignal, (_, _) =>
_window?.DispatcherQueue.TryEnqueue(() => _tray?.ShowWindow()), null, Timeout.Infinite, executeOnlyOnce: false));
Expand Down Expand Up @@ -154,6 +161,12 @@ private void WatchGuideButton()
};
_exitChord = new ControllerHoldWatcher(_window.DispatcherQueue, (ushort)(ControllerHoldWatcher.StartButton | ControllerHoldWatcher.BackButton), "Start + Back");
_exitChord.Held += () => { if (ViewModel?.IsConsoleActive == true) _ = ViewModel.RestoreNowAsync(); };
// Select + Y: the in-session menu (volume, resolution, audio, FPS, HDR, leave).
_menuChord = new ControllerHoldWatcher(_window.DispatcherQueue, (ushort)(ControllerHoldWatcher.BackButton | ControllerHoldWatcher.YButton), "Select + Y")
{
HoldDuration = TimeSpan.FromMilliseconds(250)
};
_menuChord.Held += () => ViewModel?.ToggleSessionMenu();
ViewModel.PropertyChanged += OnViewModelChangedForGuide;
RefreshGuideWatch();

Expand Down Expand Up @@ -181,10 +194,10 @@ or nameof(MainViewModel.IsConsoleActive))

private void RefreshGuideWatch()
{
if (_guide is null || _exitChord is null || ViewModel is null) return;
if (_guide is null || _exitChord is null || _menuChord is null || ViewModel is null) return;
_guide.HoldDuration = ViewModel.HomeButtonShortPress ? TimeSpan.Zero : ControllerHoldWatcher.LongHold;
if (!ViewModel.HomeButtonLaunch) { _guide.Stop(); _exitChord.Stop(); return; }
if (ViewModel.IsConsoleActive) { _guide.Stop(); _exitChord.Start(); }
else { _exitChord.Stop(); _guide.Start(); }
if (!ViewModel.HomeButtonLaunch) { _guide.Stop(); _exitChord.Stop(); _menuChord.Stop(); return; }
if (ViewModel.IsConsoleActive) { _guide.Stop(); _exitChord.Start(); _menuChord.Start(); }
else { _exitChord.Stop(); _menuChord.Stop(); _guide.Start(); }
}
}
43 changes: 2 additions & 41 deletions src/ConsoleMode/ConfirmWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Runtime.InteropServices;
using ConsoleMode.Native;
using ConsoleMode.Models;
using ConsoleMode.Services;
using Microsoft.UI.Dispatching;
Expand Down Expand Up @@ -46,7 +46,7 @@ public ConfirmWindow(ScreenRect? target, int seconds)
presenter.IsMinimizable = false;
presenter.SetBorderAndTitleBar(true, false);
}
CenterOn(appWindow, target);
WindowPlacement.CenterOn(appWindow, target, WidthDip, HeightDip);

ShowControllerHints(ControllerInput.DetectFamily());
_controller = new ControllerInput(DispatcherQueue);
Expand Down Expand Up @@ -109,43 +109,4 @@ private void UpdateCountdown()

private void OnLanguageChanged(object? sender, EventArgs e) =>
DispatcherQueue.TryEnqueue(UpdateCountdown);

private static void CenterOn(AppWindow appWindow, ScreenRect? target)
{
if (target is null || target.Width <= 0 || target.Height <= 0)
{
appWindow.Resize(new Windows.Graphics.SizeInt32((int)WidthDip, (int)HeightDip));
return;
}

// Size in the game screen's DPI (a 4K TV is usually scaled 150-300%).
var cx = target.X + target.Width / 2;
var cy = target.Y + target.Height / 2;
var scale = DpiAt(cx, cy);
var w = (int)(WidthDip * scale);
var h = (int)(HeightDip * scale);
appWindow.MoveAndResize(new Windows.Graphics.RectInt32(cx - w / 2, cy - h / 2, w, h));
}

[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int X, Y; }

[DllImport("user32.dll")]
private static extern nint MonitorFromPoint(POINT pt, uint flags);

[DllImport("shcore.dll")]
private static extern int GetDpiForMonitor(nint monitor, int dpiType, out uint dpiX, out uint dpiY);

private static double DpiAt(int x, int y)
{
try
{
var monitor = MonitorFromPoint(new POINT { X = x, Y = y }, 2 /* MONITOR_DEFAULTTONEAREST */);
return GetDpiForMonitor(monitor, 0, out var dpi, out _) == 0 && dpi > 0 ? dpi / 96.0 : 1.0;
}
catch
{
return 1.0;
}
}
}
3 changes: 3 additions & 0 deletions src/ConsoleMode/Models/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ public sealed class ConsoleRuntimeState
public bool HdrApplied { get; set; }
public string? HdrMonitor { get; set; }
public bool VrrApplied { get; set; }
/// <summary>The session menu turned off an HDR that was on before the session; Stop() turns it back on.</summary>
public bool HdrTurnedOffByUser { get; set; }
public DateTime? SessionStartedAt { get; set; }
public string? BackupAudioId { get; set; }
}

Expand Down
34 changes: 34 additions & 0 deletions src/ConsoleMode/Native/NativeWindows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,40 @@ public static class NativeWindows
[DllImport("user32.dll")]
public static extern nint GetForegroundWindow();

[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(nint hWnd);

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(nint hWnd, nint processId);

[DllImport("user32.dll")]
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);

[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();

/// <summary>
/// Bring a window in front of a fullscreen game. Windows refuses SetForegroundWindow from a
/// background process, so attach to the foreground thread's input queue first.
/// </summary>
public static bool ForceForeground(nint hwnd)
{
var current = GetForegroundWindow();
if (current == hwnd) return true;
var ourThread = GetCurrentThreadId();
var theirThread = current == 0 ? 0 : GetWindowThreadProcessId(current, 0);
var attached = theirThread != 0 && theirThread != ourThread && AttachThreadInput(ourThread, theirThread, true);
try
{
SetWindowPos(hwnd, -1 /* HWND_TOPMOST */, 0, 0, 0, 0, 0x0001 | 0x0002 | 0x0040 /* NOSIZE|NOMOVE|SHOWWINDOW */);
return SetForegroundWindow(hwnd);
}
finally
{
if (attached) AttachThreadInput(ourThread, theirThread, false);
}
}

[DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam);

Expand Down
Loading