;----------------------------------------------------------------------------------------------------------------------- ; X2: The Threat ; AutoHotKey script ; v4 (2011-Aug-12) ;----------------------------------------------------------------------------------------------------------------------- #SingleInstance force #NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases. SendMode Input ; Recommended for new scripts due to its superior speed and reliability. ; This script handles joystick trigger buttons for X2: The Threat, and allows you to switch weapons on the fly. ; You can set up sophisticated "Weapon Groups", or assign different weapons to individual buttons. It is written with ; the Saitek X52 Flight Control System in mind, but should work for other joysticks with minimal changes. ; ; Before using the script, you must first do some setup in Saitek Profile Editor (if applicable) and in the X2: The ; Threat "Button Assignments" dialog. The joystick buttons you want to capture must be *unprogrammed* in Saitek's ; software (to be sure that normal joystick button events are sent instead of key press macros). Also, the buttons ; must be *unbound* from the game, otherwise the game will capture them and the script won't be able to detect when ; they are pressed. To unbind them, Run X2.exe, click Input Device, click Button Assignments, then clear all the ; joystick buttons which this script will be handling. You can clear them by repeatedly assigned all functions to ; a single unused joystick button. If you skip this step, the script will not work. ; ; To customize the script, first edit the area indicated between the ##### lines. ; In the example below I have a Barracuda ship (2 weapons slots) with 3 types of weapons onboard: 2x Gamma Impulse Ray ; Emitter, 2x Beta High Energy Plasma Thrower and 2x Ion Disruptor. I've set up the IRE's to fire at a light press ; of the trigger, the HEPT's to fire when it is fully squeezed, and the Ion Disruptor's to fire when the pinkie ; trigger is pressed. Note in the X52 Profile Editor the pinkie is set up as a normal button, not as a "shift button". ; (I found it more comfortable to use the Fire D button on the left throttle control as the shift button instead). ; When the pinkie button and full trigger are squeezed at the same time, one HEPT and one Ion Disruptor fire ; simultaneously. Similarly, if the pinkie is held while the trigger is only partially depressed, the IRE and the Ion ; Disruptor are fired. Firing continues until you release all the buttons, and you can change combinations on the fly ; without interrupting your "hail of bullets". ; ; The script treats each combination of joystick buttons as a unique "trigger state". You define which weapons should ; be active for each state. When the script is started, it reads in the parameters you set up and figures out what ; keypresses will be required during the game to transition between states. E.g. To switch from having the two IRE's ; active, to having the two HEPTS active, it must: 1) Release the left mouse button to stop firing (if it was down), ; 2) Press the "1" key then the "2" key to cycle the weapon in each slot once, 3) Depress the left mouse button again ; to resume firing (if it was down initially). The script sends these keypresses / mouse clicks to the game so quickly ; that you don't notice any interruption to your stream of bullets, except that the ammunition changes color (and ; correspondingly, the amount of damage inflicted / energy used). ; ; If you find that kepress events seem to get "missed" or the script seems to "lose track" of which weapons are ; active in the game, then try increasing the x2_SleepDelay constant. x2_InitVars() ; Must call this function on script init ; Initialization is wrapped into a function for easier integration with other scripts. ; Must call it on script startup. x2_InitVars() { global ; Means function uses global variables by default instead of locals if (x2_DoneInit) { ; Avoid calling more than once return } ; #################################################################################################################### ; CUSTOMIZE THIS AREA (to reflect your ship and inventory) ; This must be set to the mouse button which is configured to fire weapons in X2 ; e.g. can be "LButton", "RButton" or "MButton" x2_FireButton := "LButton" ; Number of weapons slots on your ship x2_WeaponSlots := 2 ; How many of each weapon you have on board. You can name the weapons anything you'd like, as long as you ; are consistent (note they are case sensitive). Ensure there are no spaces between entries, and be sure ; to list them in the order they appear when cycling through weapons in the game (important!). x2_WeaponsOnBoard := "Gamma:2,Beta:2,Ion:2" ; Trigger state table. ; Define each trigger state according to which Joystick and / or keyboard buttons are pressed. Each string is a ; comma-delimited list of key:value pairs. The key is the button name and the value is 0 for "pressed", ; 1 for "not pressed", or * for "doesn't matter" (you can also just exclude the button from the list for "doesn't ; matter"). x2_State0 must always correspond to "no buttons pressed". ; You can add / remove states as desired, but the numeric suffices must be sequential. Each state listed here ; must have a corresponding x2_Weap# constant defined in the section below. ; For the X52, it's recommended you keep the Light Trigger / Full Trigger loadouts the same to avoid sending an ; unneccessary mouse click if you fully squeeze trigger while in menus (unless you have a light trigger finger!). ; This Trigger state table is for when all three buttons are defined as "unprogrammed" in the Saitek Profile ; Editor (i.e. Pinkie is not set up as a shift key) x2_State0 := "Joy1:0,Joy15:0,Joy6:0" ; None x2_State1 := "Joy1:1,Joy15:0,Joy6:0" ; Light Trigger x2_State2 := "Joy1:1,Joy15:0,Joy6:1" ; Light Trigger + Pinkie x2_State3 := "Joy1:*,Joy15:1,Joy6:0" ; Full Trigger x2_State4 := "Joy1:*,Joy15:1,Joy6:1" ; Full Trigger + Pinkie x2_State5 := "Joy1:0,Joy15:0,Joy6:1" ; Pinkie only ; Set these to your desired loadouts for each trigger state. ; The comma-delimited strings must contain one entry for each weapon slot on your ship. Use "None" (no quotes) to ; leave a slot empty. x2_Weap0 should correspond to the default weapon configuration (i.e. what's equipped when you ; load a savegame. x2_Weap0 := "Gamma,Gamma" ; None x2_Weap1 := "Gamma,Gamma" ; Light Trigger x2_Weap2 := "Gamma,Ion" ; Light Trigger + Pinkie x2_Weap3 := "Beta,Beta" ; Full Trigger x2_Weap4 := "Beta,Ion" ; Full Trigger + Pinkie x2_Weap5 := "Ion,Ion" ; Pinkie only ; This is an alternate Trigger state table for when the Pinkie trigger is set up as shift key. Note in this case we ; don't get joystick button events for the Pinkie trigger, so in the Saitek Profile Editor you need to assign keyboard ; keys to Pinkie + Light Trigger and Pinkie + Full Trigger. In this case, the * and / keys on the numeric keypad ; are used. ;x2_State0 := "Joy1:0,Joy15:0,NumPadMult:0,NumPadDiv:0" ; None ;x2_State1 := "Joy1:1,Joy15:0,NumPadMult:0,NumPadDiv:0" ; Light Trigger ;x2_State2 := "Joy1:*,Joy15:*,NumPadMult:1,NumPadDiv:0" ; Light Trigger + Pinkie ;x2_State3 := "Joy1:*,Joy15:1,NumPadMult:0,NumPadDiv:0" ; Full Trigger ;x2_State4 := "Joy1:*,Joy15:*,NumPadMult:*,NumPadDiv:1" ; Full Trigger + Pinkie ; This is a sample loadout for the alternate Trigger state table above. ;x2_Weap0 := "Gamma,Gamma" ; None ;x2_Weap1 := "Gamma,Gamma" ; Light Trigger ;x2_Weap2 := "Gamma,Ion" ; Light Trigger + Pinkie ;x2_Weap3 := "Beta,Beta" ; Full Trigger ;x2_Weap4 := "Ion,Ion" ; Full Trigger + Pinkie ; The script will precalculate neccessary keystrokes to switch between weapons configurations. You can override ; specific state transitions if desired. e.g: ; x2_State0To4 := "122" ; Gamma,Gamma --> Beta,Ion ; #################################################################################################################### ; Set up windows to hook. First one is for X2 game; second is for the Saitek X52 Profile Editor testing window ; (which can help with debugging) GroupAdd x2_Group, X2 ahk_class X2 GroupAdd x2_Group, Testing ahk_class WindowsForms10.Window.8.app.0.33c0d9d ; Other Globals x2_PollInterval := 30 ; How often to poll (in milliseconds) for changes after a joystick button is pressed x2_SleepDelay := 30 ; Milliseconds to wait after sending a keypress command to the game x2_States := 0 ; Number of trigger states defined; will be populated by ValidateStates() x2_Polling := false ; Whether the x2_Poll timer is enabled x2_State := 0 ; Current state ; Validate user-supplied parameters local tmp tmp := x2_ValidateStates() if (tmp != "") { FatalError(tmp, "X2 Failed Validation") } ; For testing purposes ; ;x2_CalcStateTransition(currentLoadout, desiredLoadout, warning) ; MsgBox % x2_CalcStateTransition("Alpha,ION", "Alpha,Alpha", x2_warningShown) x2_CalcStateTransitions() x2_HotKeys := x2_CalcHotKeys() x2_RegisterHotKeys() ; For debugging ;MsgBox % x2_DumpStateTransitions() ;ExitApp x2_DoneInit := true } ; X2 State Transition Precalculation --------------------------------------------------------------------------------- ; Returns blank string if validation successful x2_ValidateStates() { local idx, tmp Loop { idx := A_Index - 1 ; Make idx a loop counter which starts at 0 ; Loop until no more states defined if (!VarExists(x2_State%idx%)) { break } ; Ensure corresponding weapon loadout for this state is defined if (!VarExists(x2_Weap%idx%)) { return % "Must define x2_Weap" . idx } ; Validate loadout tmp := x2_ValidateLoadout(x2_Weap%idx%, "x2_Weap" . idx) if (tmp != "") { return tmp } } x2_States := idx return "" } x2_ValidateLoadout(loadout, loadoutName) { global x2_WeaponSlots, x2_WeaponsOnBoard if (ListCount(loadout) != x2_WeaponSlots) { return % "Incorrect number of weapons in " . loadoutName . " loadout" } ; Ensure we have enough weapons on board to satisfy this loadout loadoutTally := TallyListElements(loadout) Loop, Parse, loadoutTally, `, { SplitPair(A_LoopField, weapon, numRequired) if (weapon != "None") { if (!KeyExists(x2_WeaponsOnBoard, weapon)) { return % "Weapon " . weapon . " in " . loadoutName . " loadout not defined in x2_WeaponsOnBoard" } if (GetValForKey(x2_WeaponsOnBoard, weapon) < numRequired) { return % "Not enough " . weapon . " in x2_WeaponsOnBoard for " . loadoutName . " loadout" } } } } x2_CalcStateTransitions() { local from := 0, to, warningShown := false Loop { to := 0 Loop { if (VarExists(x2_State%from%To%to%) != 1) { ; skip if this transition was manually defined if (from != to) { x2_State%from%To%to% := x2_CalcStateTransition(x2_Weap%from%, x2_Weap%to%, WarningShown) } } to++ if (to >= x2_States) { break } } from++ if (from >= x2_States) { break } } } x2_DumpStateTransitions() { local from := 0, to, tmp := "" Loop { to := 0 Loop { if (from != to) { tmp := tmp . from . "-->" . to . ": " . PadRight(x2_State%from%To%to%, 8, " ") tmp := tmp . x2_Weap%from% . "-->" . x2_Weap%to% "`r`n" } to++ if (to >= x2_States) { break } } from++ if (from >= x2_States) { break } } return tmp } x2_CalcStateTransition(currentLoadout, desiredLoadout, ByRef warningShown) { global x2_WeaponSlots, x2_WeaponsOnBoard loadout := currentLoadout stowed := x2_CalcStowedWeapons(x2_WeaponsOnBoard, loadout) transitionDesc = from %loadout% to %desiredLoadout% macro := "" Loop, %x2_WeaponSlots% { slot := A_Index - 1 ; weapon slots start at 0 but loop counter starts at 1 desiredWeapon := GetListElement(desiredLoadout, slot) x2_ChangeWeapon(slot, desiredWeapon, desiredLoadout, macro, loadout, stowed, transitionDesc) if (x2_AllWeaponsRemoved(loadout)) { tmp := "The transition calculated " . transitionDesc . " has a step where all weapons are removed. This " tmp := tmp . "should be avoided as it will force your energy banks to recharge from zero. To avoid this " tmp := tmp . "error, make sure at least one slot below " . slot tmp := tmp . " (starting at 0) has a weapon assigned to it in x2_Weap" . slot . ". Or try adding an extra " tmp := tmp . desiredWeapon . " to your cargo.`r`n`r`n" tmp := tmp . "You can also disable this warning by setting up a manual override for this transition " tmp := tmp . "(set x2_StateXToY variable)." if (!WarningShown) { MsgBox , 48, "X2 Warning", %tmp% } } } return macro } x2_AllWeaponsRemoved(loadout) { global x2_WeaponSlots Loop %x2_WeaponSlots% { slot := A_Index - 1 if (GetListElement(loadout, slot) != "None") { return false } } return true } x2_ChangeWeapon(slot, desiredWeapon, desiredLoadout, ByRef macro, ByRef loadout, ByRef stowed, transitionDesc) { global x2_WeaponSlots, x2_WeaponsOnBoard ; If this slot already contains the weapon, nothing to do if (GetListElement(loadout, slot) = desiredWeapon) { return } ;MsgBox Changing weapon for slot %slot%; Loadout: %loadout%; DesiredLoadout: %desiredLoadout%; Macro: %macro% ; If none of the desired weapon are available, free it up from a slot we haven't visited yet if (desiredWeapon != "None") { if (GetValForKey(stowed, desiredWeapon) < 1) { ;MsgBox Freeing up %desiredWeapon%; Loadout: %loadout%; Macro: %macro% x2_FreeUpWeapon(desiredWeapon, desiredLoadout, macro, loadout, stowed, slot, transitionDesc) } } ; Cycle the slot until it contains the desired weapon while (GetListElement(loadout, slot) != desiredWeapon) { ;MsgBox Cycling slot %slot%; Loadout: %loadout%; Macro: %macro% x2_CycleWeapon(slot, macro, loadout, stowed) if (A_Index) > 50 { FatalError("Infinite loop detected in x2_ChangeWeapon while transitioning " . transitionDesc) } } } ; Searches all slots > currentSlot. If one is found that contains the given weapon but doesn't ; need it, then it cycles that slot once to free up the weapon. ; weapon: weapon we need to free up ; desiredLoadout: comma-delimited list indicating the final desired loadout. Used to avoid ; unneccessarily cycling slots. ; currentSlot: slot which needs the weapon ; transitionDesc: description of transition (for error purposes), e.g. "from Alpha,ION to Alpha,Alpha" x2_FreeUpWeapon(weapon, desiredLoadout, ByRef macro, ByRef loadout, ByRef stowed, currentSlot, transitionDesc) { global x2_WeaponSlots Loop { currentSlot++ if (currentSlot > x2_WeaponSlots - 1) { ; Could not free up - should never happen if validations passed tmp := "Cannot calculate transition " . transitionDesc . ".`r`n" tmp := tmp . "Failed to free up " . weapon . " given existing loadout " . loadout tmp := tmp . " and desired loadout " . desiredLoadout FatalError(tmp) } if (GetListElement(loadout, currentSlot) = weapon) { if (GetListElement(desiredLoadout, currentSlot) != weapon) { x2_CycleWeapon(currentSlot, macro, loadout, stowed) return } } } } ; Cycles the given weapon slot. ; Adds the appropriate keypress to the macro chain, updates the loadout, and recalculates ; stowed weapon counts. x2_CycleWeapon(slot, ByRef macro, ByRef loadout, ByRef stowed) { global x2_WeaponsOnBoard ; Add "cycle" keypress to macro that will be sent to game macro := macro . (slot + 1) ; +1, since hotkeys for slots start at 1 ; Figure out which weapon we just cycled in and update loadout newWeapon := x2_NextWeaponAfter(GetListElement(loadout, slot), x2_WeaponsOnBoard, stowed) SetListElement(loadout, slot, newWeapon) ; Recalculate available weapon counts stowed := x2_CalcStowedWeapons(x2_WeaponsOnBoard, loadout) } ; Returns a key/value list indicating how many of each weapon are available (i.e. unquipped), ; given the specified loadout. ; loadout: comma-delimited list with one entry for each slot, indicating which weapon is ; equipped (or "None"). x2_CalcStowedWeapons(x2_WeaponsOnBoard, loadout) { stowed := "" loadoutTally := TallyListElements(loadout) Loop, Parse, x2_WeaponsOnBoard, `, { SplitPair(A_LoopField, weapon, onBoard) used := GetValForKey(loadoutTally, weapon, 0) SetValForKey(stowed, weapon, onBoard - used) } return stowed } ; Returns the next weapon (or "None") that will be cycled into a slot ; weapon: currently equipped weapon ; weaponsOnBoard: key/value list indicating how many of each weapon we have (total equipped & unequipped) ; stowed: key/value list indicating how many of each weapon are available (i.e. are unequipped) x2_NextWeaponAfter(weapon, weaponsOnBoard, stowed) { if (weapon = "None") { idx := 0 } else { idx := IndexOfKey(weaponsOnBoard, weapon) + 1 } Loop { ; Break if cycled beyond last weapon (game always cycles to "None" at this point) if (idx >= ListCount(weaponsOnBoard)) { return "None" } ; For the weapon we cycled into, retreive the number available weapon := KeyPart(PairAt(weaponsOnBoard, idx)) available := ValuePart(PairAt(stowed, idx)) ; Break if we cycled to a weapon that is available if (available > 0) { return weapon } ; Otherwise try next weapond idx++ } } ; Returns a list of all keys to monitor x2_CalcHotKeys() { global local result := "" Loop, %x2_States% { idx := A_Index - 1 result := AddAllKeyValues(result, x2_State%idx%) } return AllKeys(result) } x2_RegisterHotKeys() { global Loop, Parse, x2_HotKeys, `, { HotKey, IfWinActive, ahk_group x2_Group ;MsgBox %A_LoopField% HotKey, *%A_LoopField%, x2_KeyHandler HotKey, IfWinActive } ; More info for when double-colon binding works but Send does not: ; http://www.autohotkey.com/forum/topic15428.html ; *x:: ; *x up:: } ; X2 Event Handlers ---------------------------------------------------------------------------------------------------- x2_KeyHandler: x2_Press() Return ; Converts the current state of buttons to a string for display purposes x2_GetButtonStateString() { global x2_HotKeys global x2_FireButton result := "" Loop, Parse, x2_HotKeys, `, { if (result != "") { result := result . "," } result := result . A_LoopField . ":" . GetKeyState(A_LoopField, "P") } return result } x2_Press() { if (!x2_Polling) { x2_Polling := true SetTimer x2_Poll, -1 ; process first event immediately } } x2_Poll: x2_DoPoll() return x2_DoPoll() { local newState SetTimer x2_Poll, Off newState := x2_GetTriggerState() if (newState != x2_State) { ; If no trigger action required (i.e. source/target state is 0) and weapons change macro required ; then don't do anything. This avoids unneccessary mouse button action going from Partial to Full ; trigger if the weapons loadouts are identical. if (x2_State%x2_State%to%newState% != "" || x2_State = 0 || newState = 0) { if (x2_State != 0) { ; Stop firing Send {Blind}{%x2_FireButton% Up} ; Mouse button used for firing instead of keyboard Sleep %x2_SleepDelay% ; button, for better support in menus. Otherwise } ; holding joystick down sends key event over and ; Send weapons commands ; over to X2. x2_SendKeys(x2_State%x2_State%to%newState%) if (newState != 0) { ; Resume / begin firing Send {Blind}{%x2_FireButton% DownTemp} Sleep %x2_SleepDelay% } } } x2_State := newState ; If at least one trigger button pressed, continue polling if (x2_State != 0) { SetTimer x2_Poll, %x2_PollInterval% } else { x2_Polling := false } return } ; Polls the joystick buttons and converts the result into a trigger state number x2_GetTriggerState() { local idx, tmp Loop, %x2_States% { idx := A_Index - 1 if (x2_StateActive(x2_State%idx%)) { return idx } } FatalError("Unrecognized trigger state: " . x2_GetButtonStateString()) ; can disable this line if desired return x2_State ; no match, maintain existing state tmp := "" Loop, Parse, x2_Buttons, `, { if (tmp != "") { tmp := tmp . "," } tmp := tmp . A_LoopField . ":" . GetKeyState(A_LoopField, "P") } ; Now find a match Loop, %x2_States% { idx := A_Index - 1 if (x2_State%idx% = tmp) { return idx } } FatalError("Unrecognized trigger state: " . tmp) ; can disable this line if desired return x2_State ; if wish to continue, maintain existing state } x2_StateActive(stateStr) { Loop, Parse, stateStr, `, { SplitPair(A_LoopField, k, v) if (v != "*" && GetKeyState(k, "P") != v) { return false } } return true } x2_SendKeys(macro) { if (macro = "") { return } Loop, Parse, macro { x2_SendKeyPress(A_LoopField, 1) } } x2_SendKeyPress(key, presses) { global Loop %presses% { Send {Blind}{%key% DownTemp} Sleep %x2_SleepDelay% Send {Blind}{%key% Up} Sleep %x2_SleepDelay% } } ;----------------------------------------------------------------------------------------------------------------------- ; Utility Functions for Lists of Key/Value Pairs ; A key/value pair list is something like "Name:John,Gender:Male" ;----------------------------------------------------------------------------------------------------------------------- KeyPart(pair) { SplitPair(pair, k, v) return k } ValuePart(pair) { SplitPair(pair, k, v) return v } SplitPair(pair, ByRef k, ByRef v) { StringSplit pair, pair, `: k := pair1 v := pair2 } ; Return a comma-separated list of all keys in a key/value pair list AllKeys(lst) { result := "" Loop, Parse, lst, `, { if (result != "") { result := result . "," } result := result . KeyPart(A_LoopField) } return result } KeyExists(lst, key) { return (IndexOfKey(lst, key) != -1) } ; Returns the index of the given key, or -1 if it doesn't exist IndexOfKey(lst, key) { idx := 0 Loop, Parse, lst, `, { if (KeyPart(A_LoopField) = key) { return idx } idx++ } return -1 } PairAt(lst, idx) { return GetListElement(lst, idx) } ; Returns the value associated with the given key in a key/value pair list. ; Returns "" if not found. GetValForKey(lst, key, resultIfNotFound = "") { Loop, Parse, lst, `, { SplitPair(A_LoopField, k, v) if (k = key) { return v } } return resultIfNotFound } ; Sets the value for a given key in a key/value pair list. ; If the key does not exist, it is added. SetValForKey(ByRef lst, key, val) { result := "" found := false Loop, Parse, lst, `, { if (result != "") { result := result . "," } SplitPair(A_LoopField, k, v) if (k = key) { v := val found := true } result := % result . k . ":" . v } if (!found) { if (result != "") { result := result . "," } result := % result . key . ":" . val ; MsgBox ,,,% NotFound . " Lst: " . lst . " Key: " . key . " New list: " . result } lst := result } ; Takes two key/value lists and sums the values for all keys ; All values in both lists must be numeric, and each key must only occur once. ; The returned list contains all keys from both lists. AddAllKeyValues(lst1, lst2) { Loop, Parse, lst2, `, { SplitPair(A_LoopField, k, v) tmp := GetValForKey(lst1, k) if (tmp != "") { v := v + tmp } SetValForKey(lst1, k, v) } return lst1 } ; Takes a list of comma-separated values and counts how many occurrences there are of each value. ; e.g. For the input "John,John,Sarah" it will return "John:2,Sarah:1" TallyListElements(lst) { result := "" Loop, Parse, lst, `, { cnt := GetValForKey(result, A_LoopField, 0) SetValForKey(result, A_LoopField, cnt + 1) } return result } ;----------------------------------------------------------------------------------------------------------------------- ; Utility Functions for Comma-Delimited Lists ;----------------------------------------------------------------------------------------------------------------------- ; Count number of elements in comma-delimited list ListCount(lst) { result := 0 Loop, Parse, lst, `, { result++ } return result } ; Returns the element at the given index within the comma-delimited list ; idx must be in range (starting at 0) GetListElement(lst, idx) { StringSplit el, lst, `, idx++ ; since result will be offset by return el%idx% } ; Sets the element at the given index within the comma-delimited list ; If idx is out of range, nothing is changed SetListElement(ByRef lst, idx, value) { result := "" idx := idx + 1 ; offset since A_Index starts at 1 Loop, Parse, lst, `, { if (result != "") { result := result . "," } v := (A_Index = idx) ? value : A_LoopField result := result . v } lst := result } ; Return the number of occurrences of the given value in a comma-delimited list CountOccurrencesInList(lst, val) { result := 0 Loop, Parse, lst, `, { if (A_LoopField = val) { result++ } } return result } ;----------------------------------------------------------------------------------------------------------------------- ; Other Utility Functions ;----------------------------------------------------------------------------------------------------------------------- ; Checks if a variable exists ;See http://www.autohotkey.com/forum/viewtopic.php?p=82689#82689 ;0 indicates that the variable does not exist ;1 indicates that the variable does exist and contains data ;2 indicates that the variable does exist and is empty VarExists(ByRef v) { ; Requires 1.0.46+ return &v = &n ? 0 : v = "" ? 2 : 1 } PadRight(txt, len, padChar = " ") { chars := len - StrLen(txt) if (chars > 0) { Loop, %chars% { txt := txt . padChar } } return txt } FatalError(msg, title="AutoHotKey Fatal Error") { msg = %msg%`r`n`r`nScript Aborted. DIALOG WILL CLOSE IN 15 SECONDS. ;274,448 = Icon Hand (16) + System Modal (4096) + Task Modal (8192) + Always On Top (262144) MsgBox , 274448, %title%, %msg%, 15 ExitApp }