diff --git a/FFXIManager.csproj b/FFXIManager.csproj index f9e3475..072f31c 100644 --- a/FFXIManager.csproj +++ b/FFXIManager.csproj @@ -10,9 +10,9 @@ app.manifest - 1.3.1 - 1.3.1 - 1.3.1 + 1.3.2 + 1.3.2 + 1.3.2 true diff --git a/Infrastructure/ProcessUtilityService.cs b/Infrastructure/ProcessUtilityService.cs index 5488db3..270bfda 100644 --- a/Infrastructure/ProcessUtilityService.cs +++ b/Infrastructure/ProcessUtilityService.cs @@ -119,8 +119,71 @@ public class ProcessUtilityService : IProcessUtilityService [DllImport("user32.dll")] private static extern int GetLastError(); + // **KEYBOARD STATE MANAGEMENT**: Modern keyboard input APIs + [DllImport("user32.dll")] + private static extern bool GetKeyboardState(byte[] lpKeyState); + + [DllImport("user32.dll")] + private static extern bool SetKeyboardState(byte[] lpKeyState); + + [DllImport("user32.dll")] + private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); + + // **INPUT STRUCTURES**: For SendInput API + [StructLayout(LayoutKind.Sequential)] + private struct INPUT + { + public uint type; + public INPUTUNION u; + } + + [StructLayout(LayoutKind.Explicit)] + private struct INPUTUNION + { + [FieldOffset(0)] + public MOUSEINPUT mi; + [FieldOffset(0)] + public KEYBDINPUT ki; + [FieldOffset(0)] + public HARDWAREINPUT hi; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MOUSEINPUT + { + public int dx; + public int dy; + public uint mouseData; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + private struct KEYBDINPUT + { + public ushort wVk; + public ushort wScan; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + private struct HARDWAREINPUT + { + public uint uMsg; + public ushort wParamL; + public ushort wParamH; + } + + // **INPUT CONSTANTS** + private const uint INPUT_KEYBOARD = 1; + private const uint KEYEVENTF_KEYUP = 0x0002; + private const byte VK_MENU = 0x12; // Alt key + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - + private const uint GW_HWNDNEXT = 2; private const uint GW_HWNDPREV = 3; @@ -203,31 +266,39 @@ public async Task ActivateWindowEnhancedAsync(IntPtr win await _logging.LogDebugAsync($"Initial window state: {initialState}", "ProcessUtilityService"); using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs)); - - // **PERFORMANCE**: Reduce attempts and delays for faster response + + // **SIMPLIFIED**: Only 2 attempts now (SwitchToWindow -> Standard) int attempts = 0; bool success = false; - - while (!success && attempts < 3 && !cts.Token.IsCancellationRequested) + string? strategyUsed = null; + + while (!success && attempts < 2 && !cts.Token.IsCancellationRequested) { attempts++; + strategyUsed = attempts == 1 ? "SwitchToWindow" : "Standard"; + + await _logging.LogDebugAsync($"Attempting window activation strategy {attempts}/2: {strategyUsed}", "ProcessUtilityService"); + success = await AttemptWindowActivation(windowHandle, attempts, cts.Token); - - if (!success && attempts < 3) + + if (!success && attempts < 2) { - // **PERFORMANCE**: Reduced delay between attempts - await Task.Delay(Math.Min(20 * attempts, 50), cts.Token); // Max 50ms delay + // Brief delay between strategies + await Task.Delay(20, cts.Token); } } stopwatch.Stop(); - + // **VERIFICATION**: Get final window state var finalState = GetWindowState(windowHandle); - + if (success || finalState.IsForeground) { - await _logging.LogDebugAsync($"Window activation succeeded after {attempts} attempts in {stopwatch.ElapsedMilliseconds}ms", "ProcessUtilityService"); + await _logging.LogDebugAsync( + $"Window activation SUCCESS using {strategyUsed ?? "unknown"} strategy after {attempts} attempts in {stopwatch.ElapsedMilliseconds}ms", + "ProcessUtilityService"); + var successResult = WindowActivationResult.Successful(windowHandle, stopwatch.Elapsed, attempts); successResult.WindowState = finalState; return successResult; @@ -509,6 +580,42 @@ public async Task> GetProcessesByNamesAsync(IEnumerable + /// Resets keyboard state to ensure no modifier keys are stuck. + /// **CRITICAL**: Prevents DirectX input corruption after window activation. + /// + private static void ResetKeyboardState() + { + try + { + // Create clean keyboard state (all keys UP) + byte[] cleanState = new byte[256]; + + // Get current state for diagnostic purposes + byte[] currentState = new byte[256]; + if (GetKeyboardState(currentState)) + { + // Check if any modifier keys are pressed + bool altPressed = (currentState[VK_MENU] & 0x80) != 0; + bool ctrlPressed = (currentState[0x11] & 0x80) != 0; // VK_CONTROL + bool shiftPressed = (currentState[0x10] & 0x80) != 0; // VK_SHIFT + + if (altPressed || ctrlPressed || shiftPressed) + { + System.Diagnostics.Debug.WriteLine($"[KEYBOARD RESET] Modifiers detected before reset - Alt:{altPressed} Ctrl:{ctrlPressed} Shift:{shiftPressed}"); + } + } + + // Reset to clean state + SetKeyboardState(cleanState); + System.Diagnostics.Debug.WriteLine("[KEYBOARD RESET] Keyboard state reset to clean (all keys UP)"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[KEYBOARD RESET] Error resetting keyboard state: {ex.Message}"); + } + } + private string GetWindowTitle(IntPtr hWnd) { try @@ -620,7 +727,8 @@ private static int GetWindowZOrder(IntPtr hWnd) } /// - /// Attempts window activation using progressive strategies. + /// Attempts window activation using simplified strategies optimized for DirectX games. + /// **REFACTORED**: Reduced from 3 to 2 strategies, removed thread attachment and synthetic input. /// private static async Task AttemptWindowActivation(IntPtr hWnd, int attemptNumber, CancellationToken cancellationToken) { @@ -628,178 +736,138 @@ private static async Task AttemptWindowActivation(IntPtr hWnd, int attempt switch (attemptNumber) { case 1: - // **ATTEMPT 1**: Simple activation - return await SimpleActivation(hWnd, cancellationToken); - + // **ATTEMPT 1**: SwitchToThisWindow (best for DirectX games) + return await SwitchToWindowActivation(hWnd, cancellationToken); + case 2: - // **ATTEMPT 2**: Thread attachment - return await ThreadAttachmentActivation(hWnd, cancellationToken); - - case 3: - // **ATTEMPT 3**: Aggressive activation with window restoration - return await AggressiveActivation(hWnd, cancellationToken); - + // **ATTEMPT 2**: Standard activation (ShowWindow + SetForegroundWindow) + return await StandardActivation(hWnd, cancellationToken); + default: return false; } } - private static async Task SimpleActivation(IntPtr hWnd, CancellationToken cancellationToken) - { - // **PERFORMANCE**: Check if already foreground first - if (GetForegroundWindow() == hWnd) - return true; - - if (IsIconic(hWnd)) - { - ShowWindow(hWnd, SW_RESTORE); - // **PERFORMANCE**: Reduced delay - await Task.Delay(20, cancellationToken); - } - - SetForegroundWindow(hWnd); - BringWindowToTop(hWnd); - - // **PERFORMANCE**: Reduced delay - await Task.Delay(5, cancellationToken); - return GetForegroundWindow() == hWnd; - } - - private static async Task ThreadAttachmentActivation(IntPtr hWnd, CancellationToken cancellationToken) + /// + /// **STRATEGY 1**: SwitchToThisWindow activation - optimal for DirectX games. + /// Uses native Windows API designed for game window switching. + /// + private static async Task SwitchToWindowActivation(IntPtr hWnd, CancellationToken cancellationToken) { - var currentThread = GetCurrentThreadId(); - var targetThread = GetWindowThreadProcessId(hWnd, out _); - - if (currentThread == targetThread) - { - return await SimpleActivation(hWnd, cancellationToken); - } - - bool attached = false; try { - attached = AttachThreadInput(currentThread, targetThread, true); - if (attached) + // **PERFORMANCE**: Check if already foreground first + if (GetForegroundWindow() == hWnd) { - if (IsIconic(hWnd)) - { - ShowWindow(hWnd, SW_RESTORE); - // **PERFORMANCE**: Reduced delay - await Task.Delay(20, cancellationToken); - } - - SetForegroundWindow(hWnd); - BringWindowToTop(hWnd); - ShowWindow(hWnd, SW_SHOW); - + System.Diagnostics.Debug.WriteLine("[ACTIVATION] Window already foreground, skipping"); + return true; + } + + // Restore window if minimized + if (IsIconic(hWnd)) + { + ShowWindow(hWnd, SW_RESTORE); await Task.Delay(20, cancellationToken); } - } - finally - { - if (attached) + + // **PRIMARY METHOD**: SwitchToThisWindow is most reliable for DirectX games + SwitchToThisWindow(hWnd, true); + + // **DIRECTX FIX**: 50ms delay for DirectX input reinitialization + await Task.Delay(50, cancellationToken); + + bool success = GetForegroundWindow() == hWnd; + + // **KEYBOARD STATE CLEANUP**: Always reset keyboard state after activation + ResetKeyboardState(); + + if (success) { - AttachThreadInput(currentThread, targetThread, false); + System.Diagnostics.Debug.WriteLine("[ACTIVATION] SwitchToThisWindow succeeded"); } + else + { + System.Diagnostics.Debug.WriteLine("[ACTIVATION] SwitchToThisWindow failed, will try standard activation"); + } + + return success; } - - return GetForegroundWindow() == hWnd; - } - - private static async Task AggressiveActivation(IntPtr hWnd, CancellationToken cancellationToken) - { - // Get the process ID of the target window - var threadId = GetWindowThreadProcessId(hWnd, out uint targetPid); - if (threadId == 0) - { - System.Diagnostics.Debug.WriteLine($"[ACTIVATION] Failed to get thread ID for window 0x{hWnd.ToInt64():X}"); - } - - // Allow the target process to set foreground window - AllowSetForegroundWindow((int)targetPid); - - // **ENHANCED**: Log current foreground window for debugging - var currentForeground = GetForegroundWindow(); - if (currentForeground != IntPtr.Zero) + catch (Exception ex) { - var fgState = GetWindowState(currentForeground); - System.Diagnostics.Debug.WriteLine($"[ACTIVATION] Current foreground before activation: {fgState.WindowTitle} (0x{currentForeground.ToInt64():X})"); + System.Diagnostics.Debug.WriteLine($"[ACTIVATION ERROR] SwitchToWindowActivation failed: {ex.Message}"); + ResetKeyboardState(); // Cleanup even on error + return false; } - - // Temporarily disable focus stealing prevention - IntPtr timeout = Marshal.AllocHGlobal(sizeof(uint)); + } + + /// + /// **STRATEGY 2**: Standard Windows activation fallback. + /// Uses traditional SetForegroundWindow + BringWindowToTop. + /// + private static async Task StandardActivation(IntPtr hWnd, CancellationToken cancellationToken) + { try { - // Get current timeout - SystemParametersInfo(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, timeout, 0); - uint originalTimeout = (uint)Marshal.ReadInt32(timeout); - - // Set timeout to 0 (disable focus stealing prevention) - Marshal.WriteInt32(timeout, 0); - SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, timeout, SPIF_SENDCHANGE); - - // **FIX**: Use SwitchToThisWindow for better reliability with games - // This API is more reliable for switching to game windows - SwitchToThisWindow(hWnd, true); - - // Force window to restore and show + // Get the process ID for AllowSetForegroundWindow + var threadId = GetWindowThreadProcessId(hWnd, out uint targetPid); + if (threadId == 0) + { + System.Diagnostics.Debug.WriteLine($"[ACTIVATION] Failed to get thread ID for window 0x{hWnd.ToInt64():X}"); + } + + // Allow the target process to set foreground window + AllowSetForegroundWindow((int)targetPid); + + // Log current foreground for diagnostics + var currentForeground = GetForegroundWindow(); + if (currentForeground != IntPtr.Zero && currentForeground != hWnd) + { + var fgState = GetWindowState(currentForeground); + System.Diagnostics.Debug.WriteLine($"[ACTIVATION] Current foreground: {fgState.WindowTitle} (0x{currentForeground.ToInt64():X})"); + } + + // Restore window if minimized if (IsIconic(hWnd)) { ShowWindow(hWnd, SW_RESTORE); await Task.Delay(30, cancellationToken); } - + + // Show and bring to top ShowWindow(hWnd, SW_SHOW); BringWindowToTop(hWnd); - - // Multiple activation attempts in quick succession - for (int i = 0; i < 5; i++) + + // Primary activation attempt + SetForegroundWindow(hWnd); + + // **DIRECTX FIX**: 50ms delay for DirectX input reinitialization + await Task.Delay(50, cancellationToken); + + bool success = GetForegroundWindow() == hWnd; + + // **KEYBOARD STATE CLEANUP**: Always reset keyboard state after activation + // **CRITICAL**: This prevents DirectX input corruption (movement/camera lockup) + ResetKeyboardState(); + + if (success) { - // **ENHANCED**: Try multiple activation methods - SetForegroundWindow(hWnd); - - if (i == 1) - { - // Try SwitchToThisWindow again - SwitchToThisWindow(hWnd, true); - } - - // Use SendKeys to simulate user input (bypasses focus stealing prevention) - if (i == 2) - { - // Simulate an Alt key press to trick Windows into allowing focus change - keybd_event(0x12, 0, 0, 0); // Alt key down - keybd_event(0x12, 0, 2, 0); // Alt key up - System.Diagnostics.Debug.WriteLine("[ACTIVATION] Sent Alt key to bypass focus stealing prevention"); - } - - await Task.Delay(10, cancellationToken); - - if (GetForegroundWindow() == hWnd) - { - // Restore original timeout - Marshal.WriteInt32(timeout, (int)originalTimeout); - SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, timeout, SPIF_SENDCHANGE); - System.Diagnostics.Debug.WriteLine($"[ACTIVATION] Successfully activated window after {i+1} attempts"); - return true; - } + System.Diagnostics.Debug.WriteLine("[ACTIVATION] Standard activation succeeded"); } - - // Restore original timeout - Marshal.WriteInt32(timeout, (int)originalTimeout); - SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, timeout, SPIF_SENDCHANGE); + else + { + System.Diagnostics.Debug.WriteLine("[ACTIVATION] Standard activation failed"); + } + + return success; } - finally + catch (Exception ex) { - Marshal.FreeHGlobal(timeout); + System.Diagnostics.Debug.WriteLine($"[ACTIVATION ERROR] StandardActivation failed: {ex.Message}"); + ResetKeyboardState(); // Cleanup even on error + return false; } - - return false; } - [DllImport("user32.dll")] - private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo); - /// /// Checks if a window handle is still valid and exists. ///