@@ -42,54 +42,103 @@ private void WriteFocusScript()
4242
4343Add-Type @'
4444using System;
45+ using System.Diagnostics;
4546using System.Runtime.InteropServices;
46- public class WinApi {
47- [DllImport(""user32.dll"")]
48- public static extern bool SetForegroundWindow(IntPtr hWnd);
49- [DllImport(""user32.dll"")]
50- public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
51- [DllImport(""user32.dll"")]
52- public static extern bool IsIconic(IntPtr hWnd);
53- [DllImport(""user32.dll"")]
54- public static extern bool AllowSetForegroundWindow(int dwProcessId);
55- [DllImport(""user32.dll"")]
56- public static extern IntPtr GetForegroundWindow();
57- [DllImport(""user32.dll"")]
58- public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
59- [DllImport(""kernel32.dll"")]
60- public static extern uint GetCurrentThreadId();
61- [DllImport(""user32.dll"")]
62- public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
63- }
64- '@
47+ using System.Threading;
48+
49+ public class FocusGuard {
50+ delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType,
51+ IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
52+
53+ [DllImport(""user32.dll"")] static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax,
54+ IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc,
55+ uint idProcess, uint idThread, uint dwFlags);
56+ [DllImport(""user32.dll"")] static extern bool UnhookWinEvent(IntPtr hWinEventHook);
57+ [DllImport(""user32.dll"")] static extern bool SetForegroundWindow(IntPtr hWnd);
58+ [DllImport(""user32.dll"")] static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
59+ [DllImport(""user32.dll"")] static extern bool IsIconic(IntPtr hWnd);
60+ [DllImport(""user32.dll"")] static extern bool AllowSetForegroundWindow(int dwProcessId);
61+ [DllImport(""user32.dll"")] static extern IntPtr GetForegroundWindow();
62+ [DllImport(""user32.dll"")] static extern bool AttachThreadInput(uint a, uint b, bool c);
63+ [DllImport(""kernel32.dll"")] static extern uint GetCurrentThreadId();
64+ [DllImport(""user32.dll"")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
65+ [DllImport(""user32.dll"")] static extern void GetMessage(out MSG msg, IntPtr hWnd, uint min, uint max);
66+ [DllImport(""user32.dll"")] static extern bool TranslateMessage(ref MSG msg);
67+ [DllImport(""user32.dll"")] static extern IntPtr DispatchMessage(ref MSG msg);
68+
69+ [StructLayout(LayoutKind.Sequential)]
70+ struct MSG { public IntPtr hwnd; public uint message; public IntPtr wParam;
71+ public IntPtr lParam; public uint time; public System.Drawing.Point pt; }
72+
73+ const uint EVENT_SYSTEM_FOREGROUND = 3;
74+ const uint WINEVENT_OUTOFCONTEXT = 0;
75+
76+ static IntPtr _gameHwnd;
77+ static int _gamePid;
78+ static IntPtr _hook;
79+ static bool _done;
80+
81+ static void ForceForeground() {
82+ var hwnd = _gameHwnd;
83+ if (hwnd == IntPtr.Zero) return;
84+
85+ if (IsIconic(hwnd)) ShowWindow(hwnd, 9);
86+
87+ AllowSetForegroundWindow(_gamePid);
88+
89+ var fg = GetForegroundWindow();
90+ uint dummy = 0;
91+ var fgTid = GetWindowThreadProcessId(fg, out dummy);
92+ var myTid = GetCurrentThreadId();
93+
94+ if (fgTid != 0 && fgTid != myTid) {
95+ AttachThreadInput(myTid, fgTid, true);
96+ SetForegroundWindow(hwnd);
97+ ShowWindow(hwnd, 9);
98+ AttachThreadInput(myTid, fgTid, false);
99+ } else {
100+ SetForegroundWindow(hwnd);
101+ }
102+ }
65103
66- function Invoke-SetForeground($hwnd, $procId) {
67- if ($hwnd -eq [IntPtr]::Zero) { return }
104+ static WinEventDelegate _delegate;
68105
69- if ([WinApi]::IsIconic($hwnd)) {
70- [WinApi]::ShowWindow($hwnd, 9) | Out-Null
106+ static void OnForegroundChange(IntPtr hook, uint evt, IntPtr hwnd,
107+ int obj, int child, uint thread, uint time) {
108+ if (_done) return;
109+ if (hwnd == _gameHwnd) return; // game already in foreground — OK
110+ // Something else stole focus — take it back immediately
111+ ForceForeground();
71112 }
72113
73- [WinApi]::AllowSetForegroundWindow($procId) | Out-Null
114+ public static void Run(IntPtr gameHwnd, int gamePid, int guardSeconds) {
115+ _gameHwnd = gameHwnd;
116+ _gamePid = gamePid;
117+ _done = false;
118+
119+ _delegate = new WinEventDelegate(OnForegroundChange);
120+ _hook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
121+ IntPtr.Zero, _delegate, 0, 0, WINEVENT_OUTOFCONTEXT);
74122
75- $fgHwnd = [WinApi]::GetForegroundWindow()
76- $dummy = 0
77- $fgThreadId = [WinApi]::GetWindowThreadProcessId($fgHwnd, [ref]$dummy)
78- $myThreadId = [WinApi]::GetCurrentThreadId()
123+ // Initial push
124+ ForceForeground();
79125
80- if ($fgThreadId -ne 0 -and $fgThreadId -ne $myThreadId) {
81- [WinApi]::AttachThreadInput($myThreadId, $fgThreadId, $true) | Out-Null
82- [WinApi]::SetForegroundWindow($hwnd) | Out-Null
83- [WinApi]::ShowWindow($hwnd, 9) | Out-Null
84- [WinApi]::AttachThreadInput($myThreadId, $fgThreadId, $false) | Out-Null
85- } else {
86- [WinApi]::SetForegroundWindow($hwnd) | Out-Null
126+ // Pump messages for guardSeconds — the hook fires during this loop
127+ var end = DateTime.UtcNow.AddSeconds(guardSeconds);
128+ MSG msg;
129+ while (DateTime.UtcNow < end && !_done) {
130+ Thread.Sleep(100);
131+ }
132+
133+ _done = true;
134+ UnhookWinEvent(_hook);
87135 }
88136}
137+ '@ -ReferencedAssemblies 'System.Drawing'
89138
90139$target = [System.IO.Path]::GetFileNameWithoutExtension($ExeName)
91140
92- # Step 1 — wait up to 60 s for the game process to appear
141+ # Step 1 — wait up to 60 s for the game process
93142$proc = $null
94143for ($i = 0; $i -lt 120; $i++) {
95144 Start-Sleep -Milliseconds 500
@@ -101,7 +150,7 @@ public class WinApi {
101150}
102151if ($proc -eq $null) { exit }
103152
104- # Step 2 — wait up to 30 s for a window handle to appear
153+ # Step 2 — wait up to 30 s for a window handle
105154$hwnd = [IntPtr]::Zero
106155for ($i = 0; $i -lt 60; $i++) {
107156 Start-Sleep -Milliseconds 500
@@ -113,26 +162,9 @@ public class WinApi {
113162}
114163if ($hwnd -eq [IntPtr]::Zero) { exit }
115164
116- # Step 3 — aggressively reclaim focus for 20 seconds
117- # Runs every 500 ms — if anything (splash, Task Scheduler, etc.)
118- # steals the foreground, we immediately take it back.
119- # After 20 s the game is assumed to be stable in the foreground.
120- for ($i = 0; $i -lt 40; $i++) {
121- Start-Sleep -Milliseconds 500
122-
123- $proc.Refresh()
124- if ($proc.HasExited) { break }
125-
126- # Refresh window handle — it can change as game initializes
127- if ($proc.MainWindowHandle -ne [IntPtr]::Zero) {
128- $hwnd = $proc.MainWindowHandle
129- }
130-
131- $fg = [WinApi]::GetForegroundWindow()
132- if ($fg -ne $hwnd) {
133- Invoke-SetForeground $hwnd $proc.Id
134- }
135- }
165+ # Step 3 — install foreground event hook and guard for 20 seconds.
166+ # Any window that steals focus triggers an immediate reclaim.
167+ [FocusGuard]::Run($hwnd, $proc.Id, 20)
136168" ;
137169 File . WriteAllText ( focusPs1Path , script , Encoding . UTF8 ) ;
138170 logger . Log ( "FocusGame.ps1 written." ) ;
0 commit comments