2 Commits
Author SHA1 Message Date
Andre 9747022ad6 Feature: Wrapper, Logging und Doku überarbeitet
- Debug-Logging optional gemacht, den Legacy-Wrapper portabler aufgebaut (relative Pfade, Debug-Weitergabe, Python-Fallback) und die README für Endanwender und Entwickler klarer neu strukturiert.
2026-04-18 17:33:58 +02:00
Andre a00bd1ed30 refactor: Steam-Wrapper-Logik nach focusfix.py verlagert , launch.vbs jetzt obsolet/optional
- Projektname auf FocusFix geändert
2026-04-18 16:10:59 +02:00
5 changed files with 402 additions and 260 deletions
+51 -14
View File
@@ -4,47 +4,84 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Purpose ## Purpose
Windows utility that forces a game window into the foreground after launch. Works with **Steam** (automatic process name extraction from `%command%`) and **Sunshine/Moonlight** (manual `--process` argument). FocusFix is a Windows utility that forces a game window into the foreground after launch. Works with **Steam** (directly via `%command%`) and **Sunshine/Moonlight** (manual `--process` argument).
## Architecture ## Architecture
- **`launch.vbs`** — VBScript entry point called from Steam launch options. Extracts the game executable name from `%command%` via `FileSystemObject.GetBaseName()`, passes it as `--process` to `foreground_fix.py`, then launches the actual game. No console window is created. - **`focusfix.py`** — Python script that supports two modes: `--process Name` for monitoring an already-known process, or direct Steam wrapper mode where `%command%` is passed as positional arguments. In wrapper mode it derives the process name from the target executable, launches the game itself, then polls for the game process, waits for its main window (filtered by size to skip splash screens, preferring windows with titles over titleless overlays), waits for the window size to stabilize, then brings the game window to foreground using Win32 API workarounds (`AttachThreadInput` + `Alt` key simulation to bypass `SetForegroundWindow` restrictions). Monitors for window changes (splash → main window) and re-focuses automatically. Retries up to 15 times. Debug logging is only enabled explicitly with `--debug`/`-debug`, writes to `focusfix.log`, and overwrites the file on each new script start.
- **`foreground_fix.py`** — Python script that polls for the game process, waits for its main window (filtered by size to skip splash screens, preferring windows with titles over titleless overlays), waits for the window size to stabilize, then brings the game window to foreground using Win32 API workarounds (`AttachThreadInput` + `Alt` key simulation to bypass `SetForegroundWindow` restrictions). Monitors for window changes (splash → main window) and re-focuses automatically. Retries up to 15 times. Runtime logging goes to `foreground_fix.log`. - **`launch.vbs`** — Legacy Steam wrapper. Still supported as a fallback, but not the primary user path anymore. It resolves `focusfix.py` relative to its own location, prefers a sibling `.venv\Scripts\pythonw.exe`, falls back to `pythonw.exe` from `PATH`, and forwards `-d`/`-debug`/`--debug` to `focusfix.py` without passing those flags to the game.
- **`requirements.txt`** — Source-install dependencies for `focusfix.py` (`psutil`, `pywin32`).
## Setup ## Setup
```
py -m pip install -r requirements.txt
```
Optional for development:
``` ```
py -m venv .venv py -m venv .venv
.venv\Scripts\activate .venv\Scripts\activate
pip install psutil pywin32 pip install -r requirements.txt
``` ```
## Steam Launch Options ## Steam Launch Options
Preferred for end users:
``` ```
wscript "C:\Projekte\Steam_backgroud_fix\launch.vbs" %command% "C:\Path\To\FocusFix\FocusFix.exe" %command%
``` ```
Same launch option works for all Steam games — process name is extracted automatically. Direct Python/script usage:
```
"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" %command%
```
Legacy wrapper fallback:
```
wscript "C:\Path\To\FocusFix\launch.vbs" %command%
wscript "C:\Path\To\FocusFix\launch.vbs" -debug %command%
```
All Steam variants above work generically across games with similar foreground/focus issues because the process name is derived from the target executable.
## Sunshine / Moonlight ## Sunshine / Moonlight
Use as command preparation in Sunshine (full venv path required): Preferred for end users:
``` ```
"C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe" "C:\Projekte\Steam_backgroud_fix\foreground_fix.py" --process "ProcessName" "C:\Path\To\FocusFix\FocusFix.exe" --process "ProcessName"
```
Direct Python/script usage:
```
"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" --process "ProcessName"
``` ```
## Running Manually (for testing) ## Running Manually (for testing)
``` ```
.venv\Scripts\python foreground_fix.py --process CrimsonDesert python focusfix.py --process CrimsonDesert
.venv\Scripts\python foreground_fix.py --process CrimsonDesert --timeout 180 python focusfix.py --process CrimsonDesert --timeout 180
python focusfix.py "C:\Games\CrimsonDesert.exe"
python focusfix.py -debug "C:\Games\CrimsonDesert.exe"
``` ```
`--process` is required (no default). Runtime logging goes to `foreground_fix.log`. When started with `pythonw.exe`, no console window is shown; manual runs with `python.exe` can still print argument or interpreter errors to the console. Use either `--process` or a game command. By default no log file is created. When started with `--debug` or `-debug`, runtime logging goes to `focusfix.log` and overwrites any existing file from a previous run. When started with `pythonw.exe`, no console window is shown; manual runs with `python.exe` can still print argument or interpreter errors to the console.
## Key Constants in foreground_fix.py ## Working Assumptions
- End-user docs and defaults should prefer `FocusFix.exe` over Python or `.venv`-based usage.
- Source-based usage is still supported for developers and power users through `requirements.txt`.
- `launch.vbs` is legacy/fallback behavior, not the primary integration path.
- Do not assume `.venv` exists; wrapper behavior explicitly supports a fallback to global `pythonw.exe`.
## Key Constants in focusfix.py
- `MIN_WINDOW_WIDTH = 800` — distinguishes game window from splash screen - `MIN_WINDOW_WIDTH = 800` — distinguishes game window from splash screen
- `FOCUS_RETRIES = 15` / `FOCUS_RETRY_DELAY = 2.0` — retry parameters for foreground focus - `FOCUS_RETRIES = 15` / `FOCUS_RETRY_DELAY = 2.0` — retry parameters for foreground focus
@@ -53,6 +90,6 @@ Use as command preparation in Sunshine (full venv path required):
- `SetForegroundWindow` is restricted by Windows unless the calling process owns the foreground. Workaround: simulate an `Alt` keypress + `AttachThreadInput` to the foreground thread. - `SetForegroundWindow` is restricted by Windows unless the calling process owns the foreground. Workaround: simulate an `Alt` keypress + `AttachThreadInput` to the foreground thread.
- Alt key release is in a `finally` block to prevent it from staying pressed on errors. - Alt key release is in a `finally` block to prevent it from staying pressed on errors.
- `pythonw.exe` (not `python.exe`) is required for invisible background execution. - `pythonw.exe` (not `python.exe`) is required for invisible background execution when using the Python/script path.
- VBScript `WshShell.Run` with window style `0` ensures no console window appears. - Normal user-facing guidance should point to `FocusFix.exe`; direct `focusfix.py` usage is the source/developer path.
- Window selection prefers titled windows over titleless ones (prevents overlay issues like in Wuthering Waves). - Window selection prefers titled windows over titleless ones (prevents overlay issues like in Wuthering Waves).
+233 -205
View File
@@ -1,23 +1,27 @@
# Game Foreground Fix # FocusFix - Game Foreground Fix for Windows
Windows-Hilfsprogramm, das ein Spielfenster automatisch in den Vordergrund bringt. Gedacht für Spiele wie Crimson Desert, die nach dem Start im Hintergrund landen oder von Steam, Taskleiste, Splashscreens oder Overlay-Fenstern verdeckt werden. Funktioniert mit **Steam** (automatische Prozesserkennung) und **Sunshine/Moonlight** (manueller Prozessname). Windows-Hilfsprogramm, das ein Spielfenster automatisch in den Vordergrund bringt. Gedacht für Spiele (z.b. Crimson Desert), die nach dem Start im Hintergrund landen oder von Steam, Taskleiste, Splashscreens oder Overlay-Fenstern verdeckt werden. Funktioniert mit **Steam** (direkt über `%command%`) und **Sunshine/Moonlight** (manueller Prozessname).
--- ---
## Inhaltsverzeichnis ## Inhaltsverzeichnis
- [Schnellstart](#schnellstart)
- [Für Nutzer](#fur-nutzer)
- [Für Entwickler](#fur-entwickler)
- [Problembeschreibung](#problembeschreibung) - [Problembeschreibung](#problembeschreibung)
- [Lösungsansatz](#lösungsansatz) - [Lösungsansatz](#lösungsansatz)
- [Architektur](#architektur) - [Architektur](#architektur)
- [Voraussetzungen](#voraussetzungen) - [Voraussetzungen](#voraussetzungen)
- [Installation](#installation) - [Direkte Verwendung](#direkte-verwendung)
- [Aus Quellcode ausführen](#aus-quellcode-ausfuhren)
- [Konfiguration](#konfiguration) - [Konfiguration](#konfiguration)
- [Steam](#steam) - [Steam](#steam)
- [Sunshine / Moonlight](#sunshine--moonlight) - [Sunshine / Moonlight](#sunshine--moonlight)
- [Ablauf zur Laufzeit](#ablauf-zur-laufzeit) - [Ablauf zur Laufzeit](#ablauf-zur-laufzeit)
- [Dateireferenz](#dateireferenz) - [Dateireferenz](#dateireferenz)
- [focusfix.py](#focusfixpy)
- [launch.vbs](#launchvbs) - [launch.vbs](#launchvbs)
- [foreground_fix.py](#foreground_fixpy)
- [Konfigurationsparameter](#konfigurationsparameter) - [Konfigurationsparameter](#konfigurationsparameter)
- [Windows API — Technischer Hintergrund](#windows-api--technischer-hintergrund) - [Windows API — Technischer Hintergrund](#windows-api--technischer-hintergrund)
- [Das SetForegroundWindow-Problem](#das-setforegroundwindow-problem) - [Das SetForegroundWindow-Problem](#das-setforegroundwindow-problem)
@@ -29,17 +33,61 @@ Windows-Hilfsprogramm, das ein Spielfenster automatisch in den Vordergrund bring
--- ---
## Schnellstart
### Für Nutzer
Wenn du FocusFix direkt verwenden willst, ist eine fertige `FocusFix.exe` der empfohlene Weg. Du brauchst dann weder ein Virtual Environment noch ein manuelles `pip install`.
Steam-Beispiel:
```text
"C:\Path\To\FocusFix\FocusFix.exe" %command%
```
Mit Debug-Logging:
```text
"C:\Path\To\FocusFix\FocusFix.exe" -debug %command%
```
### Für Entwickler
Wenn du FocusFix aus dem Repository starten oder weiterentwickeln willst:
```bash
cd C:\Path\To\FocusFix
py -m pip install -r requirements.txt
```
Danach kannst du `focusfix.py` direkt mit deinem Python-Interpreter oder über `pythonw.exe` verwenden.
Steam-Beispiel für die Quellcode-Nutzung mit lokalem `.venv`:
```text
"C:\Projekte\FocusFix\.venv\Scripts\pythonw.exe" "C:\Projekte\FocusFix\focusfix.py" %command%
```
Ohne lokales `.venv` und mit globalem Python im `PATH`:
```text
"pythonw.exe" "C:\Projekte\FocusFix\focusfix.py" %command%
```
## Problembeschreibung ## Problembeschreibung
Beim Start eines Spiels über Steam oder Sunshine/Moonlight öffnet sich das Spielfenster im **Hintergrund** — die Steam-Oberfläche, die Windows-Taskleiste oder andere Fenster verdecken es. Der Spieler muss manuell per Alt+Tab zum Spiel wechseln. Beim Start eines Spiels über Steam oder Sunshine/Moonlight öffnet sich das Spielfenster im **Hintergrund**. Die Steam-Oberfläche, die Windows-Taskleiste oder andere Fenster verdecken es, und der Spieler muss manuell per Alt+Tab wechseln.
Ursache: Windows vergibt den Vordergrundfokus an den Prozess, der ihn aktuell besitzt. Das Spielfenster erhält keinen automatischen Fokus, weil es von einem anderen Prozess (Steam-Launcher, Sunshine) erzeugt wurde. Ursache: Windows vergibt den Vordergrundfokus nur eingeschränkt an Prozesse, die das System als berechtigt ansieht. Zusätzlich erzeugen manche Spiele beim Start mehrere Fenster wie Splashscreens, Launcher oder titellose Overlay-Fenster, die den Fokus stören können.
Zusätzlich erzeugen manche Spiele beim Start mehrere Fenster — Splashscreens, Launcher oder titellose Overlay-Fenster — die den Fokus stören können.
## Lösungsansatz ## Lösungsansatz
Ein Python-Script überwacht den Spielstart, wartet auf das Hauptfenster des Spiels und erzwingt den Vordergrundfokus über Windows-API-Aufrufe. Ein VBScript-Wrapper sorgt bei Steam dafür, dass weder ein Konsolenfenster noch andere störende Fenster erscheinen. Das Tool ist nicht auf ein bestimmtes Spiel festgelegt, sondern auf typische Startprobleme mit ähnlichem Fensterverhalten ausgelegt. `focusfix.py` kann in zwei Modi arbeiten:
1. **Steam-Wrapper-Modus:** Das Script erhält `%command%`, leitet daraus den Prozessnamen ab, startet das Spiel selbst und überwacht anschließend den Fensterfokus.
2. **Direktmodus:** Das Script wird mit `--process Name` gestartet und überwacht einen bekannten Spielprozess.
Das Tool ist nicht auf ein bestimmtes Spiel festgelegt, sondern für viele ähnliche Fokus-Probleme mit vergleichbarem Fensterverhalten gedacht.
## Architektur ## Architektur
@@ -47,64 +95,67 @@ Ein Python-Script überwacht den Spielstart, wartet auf das Hauptfenster des Spi
Steam Launch Options / Sunshine Befehlsvorbereitung Steam Launch Options / Sunshine Befehlsvorbereitung
│ │
▼ ▼
┌──────────┐ extrahiert Prozessname ┌────────────────────┐ ┌────────────────────┐
│ launch.vbs├──── aus %command% ────────────►│ pythonw.exe │ │ pythonw.exe │
│ (nur Steam)│ + startet unsichtbar │ foreground_fix.py │ │ focusfix.py │
│ │ │ --process GameName │ │ │
│ │ startet normal │ │ │ Steam: %command% │
│ ├────────────────────────────────►│ (wartet auf Spiel) │ │ Sunshine: │
└──────────┘ %command% = Spiel.exe └────────┬───────────┘ │ --process Name │
└────────┬───────────┘
│ │
Fenster gefunden Steam-Modus:
startet das Spiel selbst
│
Prozess/Fenster gefunden
│ │
┌────────▼───────────┐ ┌────────▼───────────┐
│ 1. Fenster erkennen│ │ 1. Prozess finden │
│ 2. Größe abwarten │ │ 2. Fenster erkennen│
│ 3. Spiel fokussieren│ │ 3. Größe abwarten │
│ 4. Wechsel beobachten│ │ 4. Spiel fokussieren│
│ 5. Script beendet │ │ 5. Wechsel beobachten│
│ 6. Script beendet │
└────────────────────┘ └────────────────────┘
``` ```
**Zwei Dateien**, klare Aufgabentrennung: **Zwei Dateien**, davon eine nur noch optional:
| Datei | Sprache | Aufgabe | | Datei | Sprache | Aufgabe |
|---|---|---| |---|---|---|
| `launch.vbs` | VBScript | Startrampe (nur Steam): extrahiert Prozessname aus `%command%`, startet das Python-Script unsichtbar und leitet den Spielstart weiter | | `focusfix.py` | Python 3 | Steam-Wrapper-Modus, Direktmodus per `--process`, Prozess-Monitoring, Fenstererkennung, Fensterwechsel-Erkennung, Fokus-Erzwingung, Logging |
| `foreground_fix.py` | Python 3 | Kernlogik: Prozess-Monitoring, Fenstererkennung, Fensterwechsel-Erkennung, Fokus-Erzwingung, Logging | | `launch.vbs` | VBScript | Legacy-Wrapper für ältere Steam-Konfigurationen; für den aktuellen Ablauf nicht mehr erforderlich |
## Voraussetzungen ## Voraussetzungen
| Komponente | Version | Hinweis | | Komponente | Version | Hinweis |
|---|---|---| |---|---|---|
| Windows | 10 / 11 | Benötigt Win32 API (user32.dll) | | Windows | 10 / 11 | Benötigt Win32 API (user32.dll) |
| Python | 3.10+ | `py` Launcher muss verfügbar sein |
| pip-Pakete | `psutil`, `pywin32` | Werden im `.venv` installiert |
| Steam | Beliebig | Startoptionen müssen konfigurierbar sein | | Steam | Beliebig | Startoptionen müssen konfigurierbar sein |
| Python | 3.10+ | Nur für Quellcode-Nutzung erforderlich |
| pip-Pakete | `psutil`, `pywin32` | Nur für Quellcode-Nutzung erforderlich |
## Installation ## Direkte Verwendung
Für normale Nutzer ist eine fertige `.exe` der einfachste Weg.
## Aus Quellcode ausführen
```bash ```bash
cd C:\Projekte\Steam_backgroud_fix cd C:\Path\To\FocusFix
py -m pip install -r requirements.txt
# Virtual Environment erstellen
py -m venv .venv
# Aktivieren
.venv\Scripts\activate
# Abhängigkeiten installieren
pip install psutil pywin32
``` ```
### Dateistruktur nach Installation Optional kannst du dafür auch ein Virtual Environment verwenden. Für Entwickler ist das sinnvoll, für Endanwender aber nicht der Standardweg.
### Dateistruktur im Repository
``` ```
C:\Projekte\Steam_backgroud_fix\ C:\Path\To\FocusFix\
├── .venv\ # Python Virtual Environment ├── focusfix.py # Hauptscript
├── foreground_fix.py # Hauptscript (Kernlogik) ├── focusfix.log # Logdatei (nur bei --debug / -debug)
├── foreground_fix.log # Logdatei (wird automatisch erstellt) ├── launch.vbs # Legacy-Wrapper, optional
├── launch.vbs # VBScript-Wrapper für Steam ├── requirements.txt # Python-Abhängigkeiten für Quellcode-Nutzung
├── CLAUDE.md # Kontext für Claude Code ├── CLAUDE.md # Kontext für Claude Code
└── README.md # Diese Dokumentation └── README.md # Diese Dokumentation
``` ```
@@ -115,167 +166,152 @@ C:\Projekte\Steam_backgroud_fix\
1. Steam öffnen → **Bibliothek** 1. Steam öffnen → **Bibliothek**
2. Rechtsklick auf das Spiel → **Eigenschaften** 2. Rechtsklick auf das Spiel → **Eigenschaften**
3. Unter **Allgemein → Startoptionen** eintragen: 3. Unter **Allgemein → Startoptionen** eine der folgenden Varianten eintragen:
``` #### Variante A — Empfohlen für Nutzer: `FocusFix.exe`
wscript "C:\Projekte\Steam_backgroud_fix\launch.vbs" %command%
```text
"C:\Path\To\FocusFix\FocusFix.exe" %command%
``` ```
`launch.vbs` extrahiert den Prozessnamen automatisch aus `%command%` — **keine manuelle Anpassung pro Spiel nötig**. Die gleiche Startoption kann für beliebige Steam-Spiele mit vergleichbarem Fokus-Problem verwendet werden. Mit Debug-Logging:
```text
"C:\Path\To\FocusFix\FocusFix.exe" -debug %command%
```
Die `.exe` übernimmt denselben Wrapper-Modus wie `focusfix.py`: Sie erhält `%command%`, startet das Spiel und überwacht danach den Fokus. Diese Variante ist für Endanwender gedacht.
Die Pfade im Beispiel sind Platzhalter und müssen durch deinen tatsächlichen Installationspfad ersetzt werden.
#### Variante B — Quellcode direkt mit `focusfix.py`
```text
"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" %command%
```
`focusfix.py` extrahiert den Prozessnamen automatisch aus `%command%`, startet das eigentliche Spiel und überwacht danach den Fensterfokus. Die gleiche Startoption kann für beliebige Steam-Spiele mit vergleichbarem Fokus-Problem verwendet werden.
Die Pfade im Beispiel sind Platzhalter und müssen durch deinen tatsächlichen Installationspfad ersetzt werden.
**Erklärung:** **Erklärung:**
- `wscript` — Windows Script Host, führt VBScript ohne Konsolenfenster aus - `pythonw.exe` — startet Python ohne Konsolenfenster
- `"...\launch.vbs"` — Pfad zum VBScript-Wrapper - `"...\focusfix.py"` — Python-Script im Steam-Wrapper-Modus
- `%command%` — Steam-Platzhalter, wird zur Laufzeit durch den vollständigen Pfad zur Spiel-Executable ersetzt (z.B. `"C:\SteamLibrary\...\Game.exe"`) - `%command%` — Steam-Platzhalter, wird zur Laufzeit durch den vollständigen Pfad zur Spiel-Executable ersetzt, z.B. `"C:\SteamLibrary\...\Game.exe"`
#### Variante C — Legacy: über `launch.vbs`
Wenn du den alten VBScript-Wrapper weiterhin verwenden willst, kannst du in Steam Folgendes eintragen:
```text
wscript "C:\Path\To\FocusFix\launch.vbs" %command%
Beispiel: "C:\Projekte\FocusFix\launch.vbs" %command%
```
Mit Debug-Logging:
```text
wscript "C:\Path\To\FocusFix\launch.vbs" -debug %command%
wscript "C:\Projekte\FocusFix\launch.vbs" -debug %command%
```
`launch.vbs` extrahiert den Prozessnamen aus `%command%`, startet `focusfix.py` unsichtbar mit `--process` und startet danach das eigentliche Spiel. `-d`, `-debug` und `--debug` werden an `focusfix.py` weitergereicht und nicht an das Spiel übergeben. Diese Variante funktioniert weiterhin, ist aber nur noch als Legacy-/Fallback-Weg gedacht, weil `focusfix.py` den Wrapper-Job inzwischen selbst übernehmen kann.
Der Wrapper erwartet `focusfix.py` relativ zu seinem eigenen Speicherort. Für Python nutzt er zuerst `.venv\Scripts\pythonw.exe` im selben Ordner und fällt, falls nicht vorhanden, auf `pythonw.exe` aus der globalen Installation beziehungsweise dem `PATH` zurück.
**Erklärung:**
- `wscript` — startet VBScript ohne sichtbares Konsolenfenster
- `"...\launch.vbs"` — Legacy-Wrapper, der `focusfix.py` indirekt startet
- `-debug` — optional, aktiviert `focusfix.log` für `focusfix.py`
- `%command%` — Steam-Platzhalter mit dem ursprünglichen Spielstartbefehl
### Sunshine / Moonlight ### Sunshine / Moonlight
In Sunshine als **Befehlsvorbereitung** (Command Preparation) pro Spiel eintragen: Für Nutzer mit `.exe`:
``` ```text
"C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe" "C:\Projekte\Steam_backgroud_fix\foreground_fix.py" --process "Prozessname" "C:\Path\To\FocusFix\FocusFix.exe" --process "Prozessname"
``` ```
Den Prozessnamen im Task-Manager ablesen (ohne `.exe`). Beispiele: Mit Debug-Logging:
```text
"C:\Path\To\FocusFix\FocusFix.exe" --debug --process "Prozessname"
```
Für Quellcode-Nutzung in Sunshine als **Befehlsvorbereitung** (Command Preparation) pro Spiel:
```text
"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" --process "Prozessname"
```
Den Prozessnamen im Task-Manager ablesen, ohne `.exe`. Beispiele:
| Spiel | Prozessname | | Spiel | Prozessname |
|---|---| |---|---|
| Crimson Desert | `CrimsonDesert` | | Crimson Desert | `CrimsonDesert` |
| Wuthering Waves | `Client-Win64-Shipping` | | Wuthering Waves | `Client-Win64-Shipping` |
**Wichtig:** Der vollständige Pfad zu `pythonw.exe` im `.venv` ist nötig, damit die Abhängigkeiten (`psutil`, `pywin32`) gefunden werden. `py` oder `python` nutzt die globale Python-Installation ohne diese Pakete. **Wichtig:** Wenn du den Quellcode-Weg nutzt, muss der verwendete Python-Interpreter Zugriff auf die installierten Abhängigkeiten aus `requirements.txt` haben. `pythonw.exe` ist für unsichtbare Hintergrundausführung sinnvoller als `python.exe`.
## Ablauf zur Laufzeit ## Ablauf zur Laufzeit
``` ```text
Zeitachse Zeitachse
───────────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────────
t=0s Steam/Sunshine startet das Script mit --process "GameName" t=0s Steam startet das Script mit %command%
t=0.1s foreground_fix.py beginnt zu pollen (alle 0.5s) oder Sunshine mit --process "GameName"
→ Sucht nach Prozess "GameName"
t=0.1s focusfix.py beginnt zu arbeiten
→ Steam-Modus: startet zuerst das Spiel und leitet den Prozessnamen ab
→ Danach: sucht nach Prozess "GameName"
t=~3s Spielprozess gestartet, ggf. Splashscreen erscheint t=~3s Spielprozess gestartet, ggf. Splashscreen erscheint
→ Script findet den Prozess (PID) → Script findet den Prozess (PID)
→ Sucht nach Fenster mit Breite >= 800px → Sucht nach Fenster mit Breite >= 800px
→ Splashscreen ist kleiner → wird ignoriert → Kleine Splashscreens werden ignoriert
t=~10s Hauptfenster des Spiels erscheint (z.B. 2560×1440) t=~10s Hauptfenster des Spiels erscheint
→ Script findet das Fenster (bevorzugt Fenster mit Titel) → Script findet das Fenster
→ Wartet 3 Sekunden (Initialisierung abwarten) → Bevorzugt Fenster mit Titel
→ Wartet 3 Sekunden
→ Wartet bis Fenstergröße stabil ist → Wartet bis Fenstergröße stabil ist
t=~13s Phase 3: Fokus-Erzwingung t=~13s Phase 3: Fokus-Erzwingung
→ Spielfenster wird in den Vordergrund gebracht → Spielfenster wird in den Vordergrund gebracht
→ Bei Misserfolg: bis zu 15 Wiederholungen (je 2s Pause) → Bei Misserfolg: bis zu 15 Wiederholungen mit je 2s Pause
→ Beobachtet ob sich das Fenster ändert (Splash → Hauptfenster) → Beobachtet Fensterwechsel (z.B. Splash → Hauptfenster)
→ Ignoriert titellose Overlay-Fenster → Ignoriert problematische titellose Overlay-Fenster
t=~15s Erfolg → foreground_fix.py beendet sich t=~15s Erfolg → focusfix.py beendet sich
→ Logdatei enthält den vollständigen Ablauf → Logdatei enthält den vollständigen Ablauf
``` ```
## Dateireferenz ## Dateireferenz
### launch.vbs ### focusfix.py
```vb
Set WshShell = CreateObject("WScript.Shell")
' Prozessname aus dem ersten Argument (Steam %command%) extrahieren
Dim exePath, fso, exeName
Set fso = CreateObject("Scripting.FileSystemObject")
exePath = WScript.Arguments(0)
exeName = fso.GetBaseName(exePath)
' Foreground-Fix unsichtbar starten mit extrahiertem Prozessnamen
WshShell.Run """...\pythonw.exe"" ""...\foreground_fix.py"" --process """ & exeName & """", 0, False
' Spiel starten: Steam-Argumente zusammensetzen
Dim gameCmd, i
gameCmd = ""
For i = 0 To WScript.Arguments.Count - 1
gameCmd = gameCmd & """" & WScript.Arguments(i) & """ "
Next
If gameCmd <> "" Then
WshShell.Run gameCmd, 1, False
End If
```
**`WshShell.Run` Parameter:**
| Parameter | Wert | Bedeutung |
|---|---|---|
| Command | `"pythonw.exe" "foreground_fix.py" --process "GameName"` | Auszuführender Befehl |
| WindowStyle | `0` | Fenster komplett versteckt |
| WaitOnReturn | `False` | Nicht auf Beendigung warten (asynchron) |
**Warum `pythonw.exe` statt `python.exe`?**
`python.exe` ist eine Konsolenanwendung und erzeugt automatisch ein CMD-Fenster. `pythonw.exe` ist die "windowless"-Variante — identischer Python-Interpreter, aber ohne Konsole.
**Warum VBScript statt Batch?**
Eine `.bat`-Datei wird von `cmd.exe` ausgeführt, das immer ein Konsolenfenster öffnet — selbst wenn es sich sofort wieder schließt. VBScript über `wscript.exe` erzeugt zu keinem Zeitpunkt ein sichtbares Fenster.
### foreground_fix.py
#### Funktionsübersicht #### Funktionsübersicht
| Funktion | Beschreibung | | Funktion | Beschreibung |
|---|---| |---|---|
| `derive_process_name(command)` | Leitet den Prozessnamen aus dem ersten Element eines Spielbefehls ab |
| `launch_game(command)` | Startet das eigentliche Spiel im Steam-Wrapper-Modus |
| `find_process_pids(name)` | Sucht laufende Prozesse nach Name (Teilstring, case-insensitiv), gibt PIDs zurück | | `find_process_pids(name)` | Sucht laufende Prozesse nach Name (Teilstring, case-insensitiv), gibt PIDs zurück |
| `find_game_window(pids)` | Findet das Hauptfenster der gegebenen PIDs — bevorzugt Fenster mit Titel, dann nach Größe | | `find_game_window(pids)` | Findet das Hauptfenster der gegebenen PIDs, bevorzugt Fenster mit Titel und dann nach Größe |
| `wait_for_stable_size(hwnd)` | Wartet bis die Fenstergröße sich stabilisiert hat (kein Resize mehr) | | `wait_for_stable_size(hwnd)` | Wartet bis die Fenstergröße sich stabilisiert hat |
| `is_foreground(hwnd)` | Prüft ob ein Fenster aktuell im Vordergrund ist | | `is_foreground(hwnd)` | Prüft ob ein Fenster aktuell im Vordergrund ist |
| `bring_to_front(hwnd)` | Erzwingt den Vordergrundfokus für ein Fenster (Alt-Key + AttachThreadInput) | | `bring_to_front(hwnd)` | Erzwingt den Vordergrundfokus für ein Fenster |
| `main()` | Orchestriert den 3-Phasen-Ablauf | | `main()` | Orchestriert Startmodus, Prozesssuche, Fenstersuche und Fokus-Erzwingung |
#### `find_game_window(pids)` — Fensterauswahl #### `main()` — Ablauf
Nutzt `win32gui.EnumWindows()` um alle Top-Level-Fenster zu durchlaufen. Für jedes Fenster: 1. **Startphase:** Entweder wird der Prozessname direkt über `--process` übergeben oder im Steam-Modus aus dem Spielbefehl abgeleitet.
2. **Steam-Wrapper-Modus:** Falls ein Spielbefehl übergeben wurde, startet das Script das Spiel selbst.
3. **Phase 1 — Prozess-Suche:** Pollt alle 0,5 Sekunden nach dem Prozessnamen. Bricht nach `--timeout` Sekunden ab.
4. **Phase 2 — Fenster-Suche:** Pollt alle 0,5 Sekunden nach einem sichtbaren Fenster mit ausreichender Größe. Nach Fund: 3 Sekunden Wartezeit für die vollständige Initialisierung.
5. **Phase 3 — Fokus mit Fensterwechsel-Erkennung:** Bis zu 15 Versuche den Spielfokus zu erzwingen, mit je 2 Sekunden Pause. Beobachtet dabei, ob sich das Fenster ändert und fokussiert das neue Fenster erneut.
1. Prüft Sichtbarkeit (`IsWindowVisible`) ### launch.vbs
2. Ermittelt die zugehörige PID (`GetWindowThreadProcessId`)
3. Misst die Fenstergröße (`GetWindowRect`)
4. Filtert nach `MIN_WINDOW_WIDTH` (Standard: 800px)
Bei mehreren Treffern wird das Fenster **mit Titel bevorzugt**, dann nach Fläche sortiert. Dadurch werden sowohl Splashscreens (zu klein) als auch titellose Overlay-Fenster (kein Titel) vom Hauptfenster unterschieden. `launch.vbs` ist nur noch ein Legacy-Wrapper. Die aktuelle empfohlene Nutzung für Endanwender erfolgt über `FocusFix.exe`. Die Datei kann als Fallback im Repository bleiben, wird für den normalen Ablauf aber nicht mehr benötigt. Der Wrapper löst `focusfix.py` relativ zu seinem eigenen Ordner auf, verwendet bevorzugt `.venv\Scripts\pythonw.exe` daneben und fällt sonst auf `pythonw.exe` aus der globalen Python-Installation beziehungsweise dem `PATH` zurück. `-d`/`-debug`/`--debug` kann er an `focusfix.py` weitergeben.
#### `bring_to_front(hwnd)` — Fokus-Erzwingung
Erzwingt den Vordergrundfokus trotz Windows-Einschränkungen:
```python
try:
# Schritt 1: Alt-Taste simulieren (hebt SetForegroundWindow-Sperre auf)
keybd_event(VK_MENU, 0, 0, 0)
# Schritt 2: Thread-Input verbinden
AttachThreadInput(target_thread, foreground_thread, True)
# Schritt 3: Fenster anzeigen und fokussieren
ShowWindow(hwnd, SW_SHOW)
BringWindowToTop(hwnd)
SetForegroundWindow(hwnd)
SetFocus(hwnd)
SetActiveWindow(hwnd)
# Schritt 4: Thread-Input trennen
AttachThreadInput(target_thread, foreground_thread, False)
except Exception:
...
finally:
# Alt-Taste wird IMMER losgelassen, auch bei Fehlern
keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0)
```
Die Alt-Taste wird in einem `finally`-Block freigegeben, damit sie bei Fehlern (z.B. ungültiges Handle, fehlende Rechte) nicht systemweit gedrückt bleibt.
#### `main()` — Dreistufiger Ablauf
1. **Phase 1 — Prozess-Suche:** Pollt alle 0,5 Sekunden nach dem Prozessnamen. Bricht nach `--timeout` Sekunden ab.
2. **Phase 2 — Fenster-Suche:** Pollt alle 0,5 Sekunden nach einem sichtbaren Fenster mit ausreichender Größe. Nach Fund: 3 Sekunden Wartezeit für die vollständige Initialisierung.
3. **Phase 3 — Fokus mit Fensterwechsel-Erkennung:** Bis zu 15 Versuche den Spielfokus zu erzwingen, mit je 2 Sekunden Pause. Beobachtet dabei, ob sich das Fenster ändert (z.B. Splash → Hauptfenster) und fokussiert das neue Fenster. Titellose Overlay-Fenster werden ignoriert, wenn bereits ein Fenster mit Titel fokussiert wurde.
## Konfigurationsparameter ## Konfigurationsparameter
@@ -283,10 +319,13 @@ Die Alt-Taste wird in einem `finally`-Block freigegeben, damit sie bei Fehlern (
| Argument | Pflicht | Standard | Beschreibung | | Argument | Pflicht | Standard | Beschreibung |
|---|---|---|---| |---|---|---|---|
| `--process` | Ja | — | Teilstring des Prozessnamens (case-insensitiv, ohne `.exe`) | | `--process` | Nein | — | Teilstring des Prozessnamens (case-insensitiv, ohne `.exe`) |
| `--timeout` | Nein | `120` | Maximale Wartezeit in Sekunden für Prozess- und Fensterfund | | `--timeout` | Nein | `120` | Maximale Wartezeit in Sekunden für Prozess- und Fensterfund |
| `command` | Nein | — | Steam-Spielbefehl, z.B. `%command%`; startet das Spiel und leitet den Prozessnamen automatisch ab |
### Konstanten in `foreground_fix.py` Es muss entweder `--process` oder ein Spielbefehl übergeben werden.
### Konstanten in `focusfix.py`
| Konstante | Wert | Beschreibung | | Konstante | Wert | Beschreibung |
|---|---|---| |---|---|---|
@@ -294,28 +333,23 @@ Die Alt-Taste wird in einem `finally`-Block freigegeben, damit sie bei Fehlern (
| `FOCUS_RETRIES` | `15` | Maximale Anzahl der Fokus-Versuche | | `FOCUS_RETRIES` | `15` | Maximale Anzahl der Fokus-Versuche |
| `FOCUS_RETRY_DELAY` | `2.0` | Sekunden zwischen Fokus-Versuchen | | `FOCUS_RETRY_DELAY` | `2.0` | Sekunden zwischen Fokus-Versuchen |
### Hardcodierte Pfade in `launch.vbs` ### Relevante Pfade
| Pfad | Zweck | | Pfad | Zweck |
|---|---| |---|---|
| `C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe` | Python-Interpreter (windowless) | | `C:\Path\To\FocusFix\FocusFix.exe` | Empfohlener Pfad für Endanwender |
| `C:\Projekte\Steam_backgroud_fix\foreground_fix.py` | Hauptscript | | `C:\Path\To\Python\pythonw.exe` | Beispielpfad zum Python-Interpreter ohne Konsolenfenster, für Quellcode-Nutzung |
| `C:\Path\To\FocusFix\focusfix.py` | Beispielpfad zum Hauptscript |
Bei einer Änderung des Installationsverzeichnisses müssen diese Pfade in `launch.vbs` angepasst werden. Bei einer Änderung des Installationsverzeichnisses müssen diese Pfade in den Steam-Startoptionen oder in Sunshine angepasst werden.
## Windows API — Technischer Hintergrund ## Windows API — Technischer Hintergrund
### Das SetForegroundWindow-Problem ### Das SetForegroundWindow-Problem
Seit Windows 2000 verhindert das Betriebssystem, dass beliebige Prozesse den Vordergrundfokus "stehlen" können. `SetForegroundWindow(hwnd)` funktioniert nur, wenn: Seit Windows 2000 verhindert das Betriebssystem, dass beliebige Prozesse den Vordergrundfokus "stehlen" können. `SetForegroundWindow(hwnd)` funktioniert nur eingeschränkt. Andernfalls blinkt das Fenster lediglich in der Taskleiste.
- Der aufrufende Prozess der Vordergrundprozess ist, **oder** Da `focusfix.py` als eigenständiger Prozess läuft und nicht Teil der normalen Spiel-UI ist, erfüllt es diese Bedingungen oft nicht direkt.
- Der aufrufende Prozess von dem Vordergrundprozess gestartet wurde, **oder**
- Der letzte Eingabe-Event vom aufrufenden Prozess stammt
Andernfalls blinkt das Fenster lediglich in der Taskleiste.
Da `foreground_fix.py` als eigenständiger Prozess läuft (nicht vom Spiel gestartet), erfüllt es keine dieser Bedingungen.
### Workaround-Strategie ### Workaround-Strategie
@@ -323,83 +357,77 @@ Das Script kombiniert drei Techniken:
**1. Alt-Key-Simulation (`keybd_event`)** **1. Alt-Key-Simulation (`keybd_event`)**
Simuliert einen Tastendruck der Alt-Taste. Windows behandelt den aufrufenden Prozess danach so, als hätte der Benutzer eine Eingabe getätigt — dies hebt die `SetForegroundWindow`-Sperre auf. Simuliert einen Tastendruck der Alt-Taste. Windows behandelt den aufrufenden Prozess danach eher wie einen Prozess mit Benutzerinteraktion.
**2. Thread-Input-Verbindung (`AttachThreadInput`)** **2. Thread-Input-Verbindung (`AttachThreadInput`)**
Verbindet die Eingabewarteschlangen zweier Threads. Danach teilen sich beide Threads den gleichen Eingabefokus-Zustand. Der Ziel-Thread (Spiel) kann nun den Fokus erhalten, als wäre er Teil des Vordergrund-Threads. Verbindet die Eingabewarteschlangen zweier Threads. Dadurch kann das Ziel-Fenster zuverlässiger in den Vordergrund gebracht werden.
**3. Mehrfache Fokus-Befehle** **3. Mehrfache Fokus-Befehle**
`ShowWindow`, `BringWindowToTop`, `SetForegroundWindow`, `SetFocus` und `SetActiveWindow` adressieren jeweils einen leicht anderen Aspekt des Fenster-Fokus unter Windows. Die Kombination stellt sicher, dass das Fenster sowohl visuell im Vordergrund ist als auch Tastatur- und Mauseingaben empfängt. `ShowWindow`, `BringWindowToTop`, `SetForegroundWindow`, `SetFocus` und `SetActiveWindow` adressieren jeweils einen leicht anderen Aspekt des Fenster-Fokus unter Windows.
### Splashscreen-Erkennung ### Splashscreen-Erkennung
Viele Spiele zeigen beim Start einen Splashscreen, bevor das Hauptfenster erscheint. Beide gehören zum selben Prozess. Viele Spiele zeigen beim Start einen Splashscreen, bevor das Hauptfenster erscheint. Beide gehören zum selben Prozess.
**Unterscheidungsmerkmal: Fenstergröße.** Der Splashscreen ist typischerweise 300–600px breit. Das Spielfenster ist mindestens so groß wie die Bildschirmauflösung (z.B. 2560×1440). Der Schwellwert `MIN_WINDOW_WIDTH = 800` trennt beide zuverlässig. Das wichtigste Unterscheidungsmerkmal ist hier die Fenstergröße. Kleine Startfenster werden über `MIN_WINDOW_WIDTH = 800` aussortiert. Zusätzlich werden Fenster mit Titel bevorzugt, um titellose Overlay-Fenster nicht fälschlich als Hauptfenster zu wählen.
Zusätzlich erzeugen manche Spiele (z.B. Wuthering Waves) **titellose Overlay-Fenster** in voller Bildschirmgröße. Diese werden durch die Bevorzugung von Fenstern mit Titel in der Sortierung und durch die Fensterwechsel-Logik ignoriert.
## Manuelles Testen ## Manuelles Testen
### Script alleine testen (Spiel muss laufen) ### Direktmodus testen
```bash ```bash
cd C:\Projekte\Steam_backgroud_fix cd C:\Path\To\FocusFix
.venv\Scripts\python foreground_fix.py --process "CrimsonDesert" --timeout 5 python focusfix.py --process "CrimsonDesert" --timeout 5
``` ```
Die Laufzeit-Logs landen in `foreground_fix.log`. Bei Ausführung über `pythonw.exe` gibt es keine sichtbare Ausgabe; bei manuellen Starts mit `python.exe` können Argument- oder Interpreter-Fehler weiterhin in der Konsole erscheinen. ### Steam-Wrapper-Modus testen
### VBScript alleine testen
```bash ```bash
wscript "C:\Projekte\Steam_backgroud_fix\launch.vbs" "C:\dummy\TestGame.exe" python focusfix.py "C:\Path\To\Game\TestGame.exe"
``` ```
Sollte das Python-Script unsichtbar starten mit `--process "TestGame"`. Prüfen ob `pythonw.exe` im Task-Manager erscheint. Standardmäßig erzeugt FocusFix keine Logdatei. Mit `--debug` oder `-debug` landen die Laufzeit-Logs in `focusfix.log`; bei jedem neuen Scriptstart wird die Datei dabei überschrieben. Bei Ausführung über `pythonw.exe` gibt es keine sichtbare Ausgabe. Bei manuellen Starts mit `python.exe` können Argument- oder Interpreter-Fehler weiterhin in der Konsole erscheinen.
### Gesamtablauf testen ### Gesamtablauf testen
1. Steam-Startoptionen wie oben konfigurieren 1. Steam-Startoptionen wie oben konfigurieren.
2. Spiel über Steam starten 2. Spiel über Steam starten.
3. Erwartung: Spielfenster erscheint nach dem Ladescreen im Vordergrund, keine Taskleiste sichtbar, keine CMD- oder Script-Fenster 3. Erwartung: Spielfenster erscheint nach dem Ladescreen im Vordergrund, ohne zusätzliches Konsolenfenster.
4. `foreground_fix.log` prüfen für den detaillierten Ablauf 4. Falls mit `--debug` gestartet wurde, `focusfix.log` prüfen.
## Debugging ## Debugging
Alle Informationen werden in `foreground_fix.log` protokolliert: Für die Fehlersuche FocusFix mit `--debug` oder `-debug` starten. Dann werden alle Informationen in `focusfix.log` protokolliert; ein vorhandenes Logfile aus einem früheren Lauf wird dabei ersetzt:
- Gefundene Kandidaten-Fenster (Handle, Titel, Größe, PID) - Gefundene Kandidaten-Fenster mit Handle, Titel, Größe und PID
- Fensterwechsel und ignorierte titellose Fenster - Fensterwechsel und ignorierte titellose Fenster
- Fokus-Versuche mit Ergebnis (Erfolg/Misserfolg) - Fokus-Versuche mit Ergebnis
- Fehler mit vollständigem Traceback - Fehler mit Traceback
### Häufige Probleme ### Häufige Probleme
| Symptom | Ursache | Lösung | | Symptom | Ursache | Lösung |
|---|---|---| |---|---|---|
| Spiel startet nicht | Steam-Startoptionen fehlerhaft | Prüfen ob `%command%` am Ende steht | | Spiel startet nicht | Steam-Startoptionen fehlerhaft | Prüfen ob `%command%` am Ende steht und `.exe` oder `pythonw.exe`/`focusfix.py` korrekt referenziert werden |
| `ModuleNotFoundError: psutil` | Globales Python statt venv | Vollständigen Pfad zu `.venv\Scripts\pythonw.exe` verwenden | | `ModuleNotFoundError: psutil` | Falsche Python-Installation | `py -m pip install -r requirements.txt` ausführen oder den richtigen Python-Interpreter verwenden |
| Script findet Prozess nicht | Prozessname stimmt nicht | `--process` anpassen, Namen im Task-Manager prüfen | | Script findet Prozess nicht | Prozessname stimmt nicht oder Launcher-Kette ist ungewöhnlich | Mit `--process` testen oder Log prüfen |
| Fenster wird nicht fokussiert | Windows-Restriktion zu streng | Retry-Anzahl/Delay in Konstanten erhöhen | | Fenster wird nicht fokussiert | Windows-Restriktion zu streng | Retry-Anzahl/Delay in Konstanten erhöhen |
| Alt-Taste bleibt hängen | Spiel mit Admin-Rechten, Script ohne | Script ebenfalls als Admin starten | | Alt-Taste bleibt hängen | Spiel mit Admin-Rechten, Script ohne | Script ebenfalls als Admin starten |
| CMD-Fenster sichtbar | `python.exe` statt `pythonw.exe` | Pfad in `launch.vbs` bzw. Sunshine-Konfiguration prüfen | | CMD-Fenster sichtbar | `python.exe` statt `pythonw.exe` | Steam-/Sunshine-Konfiguration prüfen oder `.exe` verwenden |
| Verdunkeltes/blockiertes Spiel | Titelloses Overlay-Fenster | Log prüfen — sollte durch Fenster-mit-Titel-Bevorzugung gelöst sein | | Verdunkeltes/blockiertes Spiel | Titelloses Overlay-Fenster | Log prüfen; sollte meist durch Fenster-mit-Titel-Bevorzugung abgefangen werden |
### Fenster-Inspektion per PowerShell ### Fenster-Inspektion per PowerShell
Um alle Fenster eines laufenden Spielprozesses zu inspizieren:
```powershell ```powershell
Get-Process -Name GameName | Select-Object Id, Name, MainWindowTitle, MainWindowHandle Get-Process -Name GameName | Select-Object Id, Name, MainWindowTitle, MainWindowHandle
``` ```
## Bekannte Einschränkungen ## Bekannte Einschränkungen
- **Kein Admin erforderlich**, aber manche Spiele starten mit erhöhten Rechten — in dem Fall muss das Script ebenfalls als Admin laufen - **Kein Admin erforderlich**, aber manche Spiele starten mit erhöhten Rechten. Dann muss das Script ebenfalls als Admin laufen.
- **Exklusiver Vollbildmodus:** Spiele im exklusiven Vollbild (kein Borderless Windowed) können sich anders verhalten, da sie die komplette Bildschirmausgabe übernehmen - **Exklusiver Vollbildmodus:** Spiele im exklusiven Vollbild können sich anders verhalten.
- **Anti-Cheat:** Einige Anti-Cheat-Systeme könnten die `keybd_event`-Simulation oder `AttachThreadInput`-Aufrufe als verdächtig einstufen — bisher keine Probleme bekannt - **Anti-Cheat:** Manche Anti-Cheat-Systeme könnten Eingabesimulation oder Fokus-Workarounds blockieren.
- **Mehrere Monitore:** `find_game_window` wählt das Fenster unabhängig davon, auf welchem Monitor es liegt - **Mehrere Monitore:** `find_game_window` wählt das Fenster unabhängig vom Monitor.
- **Nicht universell für jedes Spiel:** Das Tool ist generisch für viele ähnliche Fälle, aber nicht garantiert für jeden Launcher, jedes Anti-Cheat-System oder jedes ungewöhnliche Fenster-/Startverhalten - **Nicht universell für jedes Spiel:** Das Tool ist generisch für viele ähnliche Fälle, aber nicht garantiert für jeden Launcher, jedes Anti-Cheat-System oder jedes ungewöhnliche Fenster-/Startverhalten.
+71 -25
View File
@@ -1,10 +1,10 @@
""" """
foreground_fix.py focusfix.py
Wartet auf den Start von CrimsonDesert.exe und bringt das Spielfenster Bringt ein gestartetes Spiel automatisch in den Vordergrund.
automatisch in den Vordergrund.
Verwendung: Verwendung:
pythonw foreground_fix.py [--timeout 120] [--process CrimsonDesert] pythonw focusfix.py [--timeout 120] --process GameName
pythonw focusfix.py [--timeout 120] "C:\\Path\\To\\Game.exe" [args...]
Abhängigkeiten (im .venv): Abhängigkeiten (im .venv):
pip install psutil pywin32 pip install psutil pywin32
@@ -13,6 +13,7 @@ Abhängigkeiten (im .venv):
import argparse import argparse
import ctypes import ctypes
import logging import logging
import subprocess
import sys import sys
import time import time
@@ -24,12 +25,10 @@ import win32process
from pathlib import Path from pathlib import Path
_LOG_PATH = Path(__file__).resolve().parent / "foreground_fix.log" _LOG_PATH = Path(__file__).resolve().parent / "focusfix.log"
logging.basicConfig( _LOGGER = logging.getLogger("focusfix")
filename=str(_LOG_PATH), _LOGGER.addHandler(logging.NullHandler())
level=logging.DEBUG, _LOGGER.propagate = False
format="%(asctime)s %(levelname)s %(message)s",
)
# Minimale Fensterbreite in Pixeln, um den Splashscreen auszuschließen. # Minimale Fensterbreite in Pixeln, um den Splashscreen auszuschließen.
MIN_WINDOW_WIDTH = 800 MIN_WINDOW_WIDTH = 800
@@ -39,6 +38,30 @@ FOCUS_RETRIES = 15
FOCUS_RETRY_DELAY = 2.0 FOCUS_RETRY_DELAY = 2.0
def derive_process_name(command: list[str]) -> str:
if not command:
raise ValueError("No game command provided")
return Path(command[0]).stem
def configure_logging(enabled: bool, log_path: Path = _LOG_PATH) -> None:
_LOGGER.handlers.clear()
if not enabled:
_LOGGER.addHandler(logging.NullHandler())
_LOGGER.setLevel(logging.CRITICAL + 1)
return
file_handler = logging.FileHandler(log_path, mode="w", encoding="utf-8")
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
_LOGGER.addHandler(file_handler)
_LOGGER.setLevel(logging.DEBUG)
def launch_game(command: list[str]) -> subprocess.Popen:
_LOGGER.info("Launching game command: %r", command)
return subprocess.Popen(command)
def find_process_pids(name: str) -> set[int]: def find_process_pids(name: str) -> set[int]:
pids = set() pids = set()
for proc in psutil.process_iter(["pid", "name"]): for proc in psutil.process_iter(["pid", "name"]):
@@ -70,7 +93,7 @@ def find_game_window(pids: set[int]) -> int | None:
height = rect[3] - rect[1] height = rect[3] - rect[1]
if width >= MIN_WINDOW_WIDTH: if width >= MIN_WINDOW_WIDTH:
title = win32gui.GetWindowText(hwnd) title = win32gui.GetWindowText(hwnd)
logging.debug("Candidate window: hwnd=%s title=%r size=%dx%d pid=%d", hwnd, title, width, height, pid) _LOGGER.debug("Candidate window: hwnd=%s title=%r size=%dx%d pid=%d", hwnd, title, width, height, pid)
found.append((hwnd, title, width, height)) found.append((hwnd, title, width, height))
return True return True
@@ -103,7 +126,7 @@ def wait_for_stable_size(hwnd: int, polls: int = 3, interval: float = 0.5,
stable_count = 0 stable_count = 0
last_size = size last_size = size
time.sleep(interval) time.sleep(interval)
logging.debug("Stable size for hwnd=%s: %dx%d (after %d stable polls)", _LOGGER.debug("Stable size for hwnd=%s: %dx%d (after %d stable polls)",
hwnd, last_size[0], last_size[1], stable_count) hwnd, last_size[0], last_size[1], stable_count)
return last_size return last_size
@@ -142,11 +165,11 @@ def bring_to_front(hwnd: int) -> bool:
ctypes.windll.user32.AttachThreadInput(tgt_tid, fg_tid, False) ctypes.windll.user32.AttachThreadInput(tgt_tid, fg_tid, False)
result = is_foreground(hwnd) result = is_foreground(hwnd)
logging.debug("bring_to_front result: %s (foreground hwnd=%s, target hwnd=%s)", _LOGGER.debug("bring_to_front result: %s (foreground hwnd=%s, target hwnd=%s)",
result, win32gui.GetForegroundWindow(), hwnd) result, win32gui.GetForegroundWindow(), hwnd)
return result return result
except Exception as e: except Exception as e:
logging.exception("bring_to_front failed: %s", e) _LOGGER.exception("bring_to_front failed: %s", e)
return False return False
finally: finally:
win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0) win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
@@ -155,13 +178,36 @@ def bring_to_front(hwnd: int) -> bool:
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-d", "--debug", "-debug", action="store_true",
help="Debug-Logging nach focusfix.log aktivieren")
parser.add_argument("--timeout", type=int, default=120) parser.add_argument("--timeout", type=int, default=120)
parser.add_argument("--process", type=str, required=True, parser.add_argument("--process", type=str,
help="Prozessname des Spiels (ohne .exe)") help="Prozessname des Spiels (ohne .exe)")
args = parser.parse_args() parser.add_argument("command", nargs="*",
help="Steam-Spielbefehl, z.B. %%command%%")
args, extra_command_args = parser.parse_known_args()
configure_logging(args.debug)
process_name = args.process
command = args.command + extra_command_args
if command:
if process_name:
parser.error("Use either --process or a game command, not both.")
try:
process_name = derive_process_name(command)
except ValueError as exc:
parser.error(str(exc))
try:
launch_game(command)
except OSError as exc:
_LOGGER.exception("Failed to launch game command: %s", exc)
sys.exit(1)
elif not process_name:
parser.error("Either --process or a game command is required.")
deadline = time.time() + args.timeout deadline = time.time() + args.timeout
process_name = args.process _LOGGER.info("Monitoring process name: %s", process_name)
# --- Phase 1: Warten auf den Prozess --- # --- Phase 1: Warten auf den Prozess ---
pids: set[int] = set() pids: set[int] = set()
@@ -179,13 +225,13 @@ def main() -> None:
pids = find_process_pids(process_name) pids = find_process_pids(process_name)
hwnd = find_game_window(pids) hwnd = find_game_window(pids)
if hwnd: if hwnd:
logging.info("Initial window found: hwnd=%s title=%r", hwnd, win32gui.GetWindowText(hwnd)) _LOGGER.info("Initial window found: hwnd=%s title=%r", hwnd, win32gui.GetWindowText(hwnd))
# Warten bis das Spiel sein Fenster vollständig aufgebaut hat # Warten bis das Spiel sein Fenster vollständig aufgebaut hat
time.sleep(3) time.sleep(3)
break break
time.sleep(0.5) time.sleep(0.5)
else: else:
logging.error("Timeout waiting for game window") _LOGGER.error("Timeout waiting for game window")
sys.exit(1) sys.exit(1)
# --- Phase 3: Spielfenster fokussieren und auf Fensterwechsel reagieren --- # --- Phase 3: Spielfenster fokussieren und auf Fensterwechsel reagieren ---
@@ -195,7 +241,7 @@ def main() -> None:
for attempt in range(FOCUS_RETRIES): for attempt in range(FOCUS_RETRIES):
pids = find_process_pids(process_name) pids = find_process_pids(process_name)
if not pids: if not pids:
logging.warning("Game process gone, stopping") _LOGGER.warning("Game process gone, stopping")
break break
current_hwnd = find_game_window(pids) current_hwnd = find_game_window(pids)
@@ -205,10 +251,10 @@ def main() -> None:
old_title = win32gui.GetWindowText(hwnd) if hwnd else "" old_title = win32gui.GetWindowText(hwnd) if hwnd else ""
# Nicht von einem Fenster mit Titel auf ein titelloses wechseln # Nicht von einem Fenster mit Titel auf ein titelloses wechseln
if not new_title and old_title: if not new_title and old_title:
logging.debug("Ignoring titleless window hwnd=%s (keeping %s title=%r)", _LOGGER.debug("Ignoring titleless window hwnd=%s (keeping %s title=%r)",
current_hwnd, hwnd, old_title) current_hwnd, hwnd, old_title)
else: else:
logging.info("Window changed: old=%s new=%s title=%r", _LOGGER.info("Window changed: old=%s new=%s title=%r",
hwnd, current_hwnd, new_title) hwnd, current_hwnd, new_title)
hwnd = current_hwnd hwnd = current_hwnd
focused_hwnd = None # neues Fenster muss neu fokussiert werden focused_hwnd = None # neues Fenster muss neu fokussiert werden
@@ -226,15 +272,15 @@ def main() -> None:
# Warten bis Fenstergröße stabil ist (Resize abgeschlossen) # Warten bis Fenstergröße stabil ist (Resize abgeschlossen)
wait_for_stable_size(hwnd) wait_for_stable_size(hwnd)
logging.info("Focus attempt %d/%d for hwnd=%s title=%r", _LOGGER.info("Focus attempt %d/%d for hwnd=%s title=%r",
attempt + 1, FOCUS_RETRIES, hwnd, win32gui.GetWindowText(hwnd)) attempt + 1, FOCUS_RETRIES, hwnd, win32gui.GetWindowText(hwnd))
success = bring_to_front(hwnd) success = bring_to_front(hwnd)
if success: if success:
logging.info("Focus succeeded on attempt %d", attempt + 1) _LOGGER.info("Focus succeeded on attempt %d", attempt + 1)
focused_hwnd = hwnd focused_hwnd = hwnd
time.sleep(FOCUS_RETRY_DELAY) time.sleep(FOCUS_RETRY_DELAY)
logging.info("Done. Final focused hwnd=%s", focused_hwnd) _LOGGER.info("Done. Final focused hwnd=%s", focused_hwnd)
if __name__ == "__main__": if __name__ == "__main__":
+40 -11
View File
@@ -1,21 +1,50 @@
Set WshShell = CreateObject("WScript.Shell") Set WshShell = CreateObject("WScript.Shell")
' Prozessname aus dem ersten Argument (Steam %command%) extrahieren
Dim exePath, fso, exeName
Set fso = CreateObject("Scripting.FileSystemObject") Set fso = CreateObject("Scripting.FileSystemObject")
exePath = WScript.Arguments(0)
exeName = fso.GetBaseName(exePath)
' Foreground-Fix Script unsichtbar starten (pythonw = kein Konsolenfenster) Dim baseDir, pythonwPath, focusfixPath
WshShell.Run """C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe"" ""C:\Projekte\Steam_backgroud_fix\foreground_fix.py"" --process """ & exeName & """", 0, False baseDir = fso.GetParentFolderName(WScript.ScriptFullName)
pythonwPath = fso.BuildPath(baseDir, ".venv\Scripts\pythonw.exe")
focusfixPath = fso.BuildPath(baseDir, "focusfix.py")
' Spiel starten: alle Argumente (= Steam %command%) zusammensetzen und ausführen If Not fso.FileExists(pythonwPath) Then
Dim gameCmd, i pythonwPath = "pythonw.exe"
End If
Dim debugEnabled, exePath, exeName, gameCmd, arg, i
debugEnabled = False
exePath = ""
gameCmd = "" gameCmd = ""
For i = 0 To WScript.Arguments.Count - 1 For i = 0 To WScript.Arguments.Count - 1
gameCmd = gameCmd & """" & WScript.Arguments(i) & """ " arg = WScript.Arguments(i)
If arg = "-d" Or arg = "-debug" Or arg = "--debug" Then
debugEnabled = True
Else
If exePath = "" Then
exePath = arg
End If
gameCmd = gameCmd & """" & arg & """ "
End If
Next Next
If gameCmd <> "" Then If exePath = "" Then
WScript.Quit 1
End If
' Prozessname aus dem ersten nicht-Wrapper-Argument (Steam %command%) extrahieren
exeName = fso.GetBaseName(exePath)
' FocusFix Script unsichtbar starten (pythonw = kein Konsolenfenster)
Dim focusfixCmd
focusfixCmd = """" & pythonwPath & """ """ & focusfixPath & """"
If debugEnabled Then
focusfixCmd = focusfixCmd & " -debug"
End If
focusfixCmd = focusfixCmd & " --process """ & exeName & """"
WshShell.Run focusfixCmd, 0, False
' Spiel starten: alle übrigen Argumente (= Steam %command%) zusammensetzen und ausführen
If Trim(gameCmd) <> "" Then
WshShell.Run gameCmd, 1, False WshShell.Run gameCmd, 1, False
End If End If
+2
View File
@@ -0,0 +1,2 @@
psutil
pywin32