Skip to content

Commit bd8284b

Browse files
committed
fix: replace polling with SetWinEventHook for instant foreground reclaim
1 parent e6785d1 commit bd8284b

2 files changed

Lines changed: 111 additions & 59 deletions

File tree

.github/workflows/release.yml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: Build and Release
33
on:
44
push:
55
tags:
6-
- "v*" # Dispara quando você fizer push de uma tag como v1.0.1
6+
- "v*"
77

88
jobs:
99
release:
@@ -37,8 +37,28 @@ jobs:
3737
Rename-Item -Path "tmp.zip" -NewName $pextName
3838
echo "PEXT_NAME=$pextName" >> $env:GITHUB_OUTPUT
3939
40+
- name: Extrair notas do CHANGELOG
41+
id: changelog
42+
run: |
43+
$tag = "${{ github.ref_name }}"
44+
$content = Get-Content CHANGELOG.md -Raw
45+
46+
# Captura o bloco entre o cabeçalho da tag atual e o próximo cabeçalho ##
47+
$pattern = "(?s)##\s*\[$tag\][^\n]*\n(.+?)(?=\n## \[|$)"
48+
$match = [regex]::Match($content, $pattern)
49+
50+
if ($match.Success) {
51+
$notes = $match.Groups[1].Value.Trim()
52+
} else {
53+
$notes = "See CHANGELOG.md for details."
54+
}
55+
56+
# Salva em arquivo para evitar problemas com caracteres especiais
57+
Set-Content -Path "release_notes.md" -Value $notes -Encoding UTF8
58+
echo "NOTES_FILE=release_notes.md" >> $env:GITHUB_OUTPUT
59+
4060
- name: Publicar Release no GitHub
4161
uses: softprops/action-gh-release@v2
4262
with:
4363
files: ${{ steps.pext.outputs.PEXT_NAME }}
44-
generate_release_notes: true
64+
body_path: ${{ steps.changelog.outputs.NOTES_FILE }}

LauncherManager.cs

Lines changed: 89 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -42,54 +42,103 @@ private void WriteFocusScript()
4242
4343
Add-Type @'
4444
using System;
45+
using System.Diagnostics;
4546
using 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
94143
for ($i = 0; $i -lt 120; $i++) {
95144
Start-Sleep -Milliseconds 500
@@ -101,7 +150,7 @@ public class WinApi {
101150
}
102151
if ($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
106155
for ($i = 0; $i -lt 60; $i++) {
107156
Start-Sleep -Milliseconds 500
@@ -113,26 +162,9 @@ public class WinApi {
113162
}
114163
if ($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

Comments
 (0)