#Requires AutoHotkey v2.0 #SingleInstance Force ; ============================================================ ; FS Golf shot-mode hotkeys for GSPro ; by The Moo Projects -- themooprojects.com/fsgolfhotkey ; ------------------------------------------------------------ ; Switch the FlightScope FS Golf launch monitor between ; full-swing and chipping detection WITHOUT leaving GSPro. ; ; HOTKEYS ; F6 Switch to FULL SWING (activate FS Golf, click, return) ; F7 Switch to CHIPPING (activate FS Golf, click, return) ; F10 Aim test: move cursor to Full Swing button (NO click) ; F11 Aim test: move cursor to Chipping button (NO click) ; F2 CALIBRATE Full Swing (hover the button, then press F2) ; F3 CALIBRATE Chipping (hover the button, then press F3) ; ; FIRST-TIME SETUP ; 1. Open FS Golf so its window is visible at a normal size. ; 2. Hover your mouse over the "Full Swing" button, press F2. ; 3. Hover your mouse over the "Chipping" button, press F3. ; That's it -- your positions are saved next to this script in ; fsgolf-hotkey.ini and reused every time. Re-run F2/F3 anytime ; the buttons move. ; ; WHY IT WORKS THE WAY IT DOES (learned the hard way) ; - FS Golf is a WPF app with no addressable sub-controls, so ; ControlClick does NOT work -- only a real screen click does. ; - Some PCs block *injected* mouse input, so this script moves ; the cursor with SetCursorPos and sends the click as a posted ; window message (ClickMethod "post"). If that ever fails on ; your machine, set ClickMethod := "inject" below. ; - Button positions are stored as a fraction of the window size, ; so they stay correct on any monitor, resolution, or display ; scaling (e.g. a TV vs a projector) with no re-editing. ; ============================================================ ; ----- FS Golf window identity ----- FSGolfExe := "ahk_exe FlightScopeVideoTeachingApp.exe" ; ----- Click method ----- ; "post" = send the click as a window message (works when the PC ; blocks injected mouse input -- the safe default). ; "inject" = hardware-style click via mouse_event (fallback). ClickMethod := "post" ; ----- Calibration storage ----- IniFile := A_ScriptDir "\fsgolf-hotkey.ini" ; ----- Button offsets from FS Golf's top-left, and the window size ; they were measured against. These are just defaults; your ; own F2/F3 calibration overwrites them in the .ini file. ---- FullSwingOffX := 1096 FullSwingOffY := 65 ChipOffX := 1250 ChipOffY := 66 RefWinW := 1440 RefWinH := 768 ; Interpret all mouse coordinates as absolute SCREEN coordinates. CoordMode "Mouse", "Screen" CoordMode "ToolTip", "Screen" ; DPI-aware so screen coords are not rescaled at >100% scaling. DllCall("SetThreadDpiAwarenessContext", "ptr", -4, "ptr") LoadConfig() ; ------------------- Real switch hotkeys ------------------- F6::ClickShotMode("full", "FULL SWING") F7::ClickShotMode("chip", "CHIPPING") ; ------------------- Aim-test hotkeys (no click) ----------- F10::AimTest("full", "AIM: FULL SWING") F11::AimTest("chip", "AIM: CHIPPING") ; ------------------- Calibration hotkeys ------------------- F2::Calibrate("full") F3::Calibrate("chip") ; ============================================================ ; Functions ; ============================================================ LoadConfig() { global IniFile, FullSwingOffX, FullSwingOffY, ChipOffX, ChipOffY, RefWinW, RefWinH if FileExist(IniFile) { FullSwingOffX := Integer(IniRead(IniFile, "FullSwing", "OffX", FullSwingOffX)) FullSwingOffY := Integer(IniRead(IniFile, "FullSwing", "OffY", FullSwingOffY)) ChipOffX := Integer(IniRead(IniFile, "Chipping", "OffX", ChipOffX)) ChipOffY := Integer(IniRead(IniFile, "Chipping", "OffY", ChipOffY)) RefWinW := Integer(IniRead(IniFile, "Reference", "WinW", RefWinW)) RefWinH := Integer(IniRead(IniFile, "Reference", "WinH", RefWinH)) } else { ShowTip("Welcome! Not calibrated yet.`nHover the Full Swing button and press F2,`nthen hover Chipping and press F3.", 4000) } } Calibrate(which) { global FSGolfExe, IniFile, FullSwingOffX, FullSwingOffY, ChipOffX, ChipOffY, RefWinW, RefWinH hwnd := WinExist(FSGolfExe) if !hwnd { ShowTip("FS Golf window not found -- open it first.", 1800) return } ; Record where the mouse is now, relative to the window's top-left, ; plus the current window size as the reference for scaling. MouseGetPos(&mx, &my) WinGetPos(&wx, &wy, &ww, &wh, FSGolfExe) offX := mx - wx offY := my - wy RefWinW := ww RefWinH := wh IniWrite(RefWinW, IniFile, "Reference", "WinW") IniWrite(RefWinH, IniFile, "Reference", "WinH") if (which = "full") { FullSwingOffX := offX, FullSwingOffY := offY IniWrite(offX, IniFile, "FullSwing", "OffX") IniWrite(offY, IniFile, "FullSwing", "OffY") ShowTip("Saved FULL SWING`noffset " offX ", " offY " (win " ww "x" wh ")", 2000) } else { ChipOffX := offX, ChipOffY := offY IniWrite(offX, IniFile, "Chipping", "OffX") IniWrite(offY, IniFile, "Chipping", "OffY") ShowTip("Saved CHIPPING`noffset " offX ", " offY " (win " ww "x" wh ")", 2000) } } ClickShotMode(which, label) { global FSGolfExe, ClickMethod ; Remember what's currently focused (usually GSPro). prevWin := WinActive("A") MouseGetPos(&savedX, &savedY) hwnd := ResolveButton(which, &x, &y, true) if !hwnd { ShowTip("FS Golf window not found", 1200) return } ; Move the real cursor onto the button (hover), with a small ; jiggle so WPF registers the pointer, then click. MoveCursor(x - 3, y - 3) Sleep(30) MoveCursor(x, y) Sleep(120) if (ClickMethod = "post") PostClick(hwnd, x, y) else HardClick() Sleep(60) MoveCursor(savedX, savedY) if prevWin WinActivate("ahk_id " prevWin) ShowTip(label, 900) } AimTest(which, label) { hwnd := ResolveButton(which, &x, &y, false) if !hwnd { ShowTip("FS Golf window not found", 1500) return } MoveCursor(x, y) ShowTip(label " (" x ", " y ")", 1500) } ; Resolve a button ("full"/"chip") to a live screen coordinate. ; Returns FS Golf HWND (0 if not found), writes coords to &sx/&sy. ResolveButton(which, &sx, &sy, activate) { global FSGolfExe, RefWinW, RefWinH global FullSwingOffX, FullSwingOffY, ChipOffX, ChipOffY hwnd := WinExist(FSGolfExe) if !hwnd { sx := 0, sy := 0 return 0 } offX := (which = "full") ? FullSwingOffX : ChipOffX offY := (which = "full") ? FullSwingOffY : ChipOffY if activate { WinActivate(FSGolfExe) WinWaitActive(FSGolfExe, , 2) } ; Un-maximize/minimize so proportions match the calibrated size. if (WinGetMinMax(FSGolfExe) != 0) WinRestore(FSGolfExe) if activate Sleep(150) ; Scale the offset by (live size / reference size) so it stays ; correct on any monitor / scaling / resolution. WinGetPos(&wx, &wy, &ww, &wh, FSGolfExe) sx := wx + Round(offX * ww / RefWinW) sy := wy + Round(offY * wh / RefWinH) return hwnd } MoveCursor(x, y) { DllCall("SetCursorPos", "int", x, "int", y) } HardClick() { ; mouse_event: LEFTDOWN = 0x0002, LEFTUP = 0x0004 DllCall("mouse_event", "uint", 0x0002, "uint", 0, "uint", 0, "uint", 0, "uptr", 0) Sleep(60) DllCall("mouse_event", "uint", 0x0004, "uint", 0, "uint", 0, "uint", 0, "uptr", 0) } ; Post the click straight to the window (bypasses input filters). PostClick(hwnd, screenX, screenY) { pt := Buffer(8, 0) NumPut("int", screenX, pt, 0) NumPut("int", screenY, pt, 4) DllCall("ScreenToClient", "ptr", hwnd, "ptr", pt) cx := NumGet(pt, 0, "int") cy := NumGet(pt, 4, "int") lp := (cy << 16) | (cx & 0xFFFF) ; WM_MOUSEMOVE 0x0200, WM_LBUTTONDOWN 0x0201, WM_LBUTTONUP 0x0202 PostMessage(0x0200, 0, lp, , "ahk_id " hwnd) Sleep(20) PostMessage(0x0201, 1, lp, , "ahk_id " hwnd) Sleep(40) PostMessage(0x0202, 0, lp, , "ahk_id " hwnd) } ShowTip(text, ms) { ToolTip(text) SetTimer(() => ToolTip(), -Abs(ms)) }