Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

How to generate nonintrusive automations with AHK


  • Please log in to reply
4 replies to this topic
RHCP
  • Members
  • 1228 posts
  • Last active: Apr 08 2017 06:17 PM
  • Joined: 29 May 2006

To clarify the title, this topic will look at creating short automations which run seamlessly in the background and do not interfere with the users input, nor are the automations themselves affected by user input.

These two points sound easy, but are quite difficult with the tool set provided in the beautiful language which is AHK. 

 
This method revolves around using custom low level input hooks and exploiting AHK's single threaded nature.
  
The basic procedure looks like this:

Install low level (LL) keyboard and mouse hooks
Alter your LL hooks to only allow injected/synthetic keystrokes through.
Record and then Release physically pressed keys using SendInput 
If a key was released, sleep for a short amount of time 15ms seems adequate
Place the thread in critical mode and increase AHks delay in checking its internal message queue (e.g. critical, 1000)
 
Reset your LL hooks so they allow user input
Perform your automations using postmessage 
If required Restore physically pressed  keys using postmessage (The ones we previously released)
Turn off critical

 

 
Some notes:
 
It's best to be release physically pressed keys using SendInput, as using postmessage can lead to keys being stuck (seen as down) outside of the program we are using. There are some situations where you do not want to actually restore the users pressed keys. For example if the user is typing in chat/text box and the 'a' key is currently down, releasing this key with postmessage (or sendinput) will produce the 'a' character which is good, but if we were to restore this key, then when the user physically releases the key a second 'a' character will be generated.  Unfortunately, since we used postmessage to release the key everything but our program of interest will still see the 'a' as being down.
 
As AHK is single threaded, placing the automation thread into critical prevents any callbacks to the LL hooks (i.e. the user pressing or releasing keys) being processed until after we finish our automation and the thread either ends or comes out of critical. 
 
The automation must be performed using postmessage, as any sendInput command will not be processed until after the thread comes out of critical.
 
Postmessage has a lower latency than sendinput, consequently a small sleep (see the note below about sleep) is required between using a sendinput command and then posting a message to a window. Without this delay some of the postmessage input may arrive before the actual sendinput!
 
Once you have activated critical, you can not use AHKs normal sleep command or any AHK command which has an internal sleep component. This is because a sleep causes AHK to check its internal message queue which will result in it immediately processing any calls to the LL hooks. In other words, this will allow user input to interrupt the automation! This also means that you can't use controlsend/controlclick with any keydelay.
 
If the automation requires a sleep, for example to allow some action to register in the window before deciding on how to proceed, then you can use 
 

DllCall("Sleep", Uint, x)

 
 where x is the time in milliseconds. This command will sleep the entire AHK program. 
 
Unfortunately using controlsend without any key delay can result in the window ignoring some commands. A simple workaround would be to use a series of controlsend commands with a DLLcall sleep between them. Although sendcontrol is fast, it does have some overhead associated with it and in testing i found repeated controlsends took around 10x longer than a similar postmessage function. Although I have misplaced my testing data, if the automation string is quite long this delay can become quite substantial - at least compared to the overall time of the automation.
I will include some functions I wrote which can be used as alternatives to controlsend and controlclick. These functions are undoubtedly less robust than their counterparts (and may have a bug), but so far they have worked perfectly for my needs.
 
LowLevel HookTime outs:
Although this this is intended for fast automations say less than 50ms, it works perfectly fine for longer ones except for one issue. Windows monitors the time it takes for a program to process info it sends via the low level hook. If our automation takes longer than 300ms windows will automatically pass the information on to the next program in the hook chain which will result in the user input occuring before our automation finishes. If the hook times out 10 times (not sure about the exact number) then windows will permanently (and on vista onwards silently) remove our low level hook from the hook chain.
 
You can increase the hook time out interval using

 

RegWrite, REG_DWORD, HKEY_CURRENT_USER, Control Panel\Desktop, LowLevelHooksTimeout, x   ; default = 300 

 

where X is the time in milliseconds. This requires a restart to take effect.
 
 

The command:

input.hookBlock(True, True) 

is used to prevent any user keypress affecting the automation before the thread enters critical. As this is such a short timing window, this probably isn't necessary but it seems to work well. I've often thought I should also allow user key_Up messages through too, what do other think?

 

Lets look at a basic example. You will need to set the windows LowLevelHooksTimeout value to 4000 and restart your computer. This will allow plenty of time for you to to press buttons while the automation is occurring. 

Open notepad and run the test script (you will need to include the functions at the end of this post).  Press F1 and then when you hear the beep press any sequence of keys you want. Notice the keys you pressed will show up after the posted "abc123". You will also notice that the mouse cursor won't move while the thread is in critical, but once the thread finishes the mouse does indeed move the correct distance. This delay isn't noticeable for short automatons.

 

Note, the presence of low level hooks can impair sendInput. For example, I had a function bound to Alt+Tab which would often fail while the LLHooks were installed. Simply removing the hooks before the using sendInput fixed this. AHK even removes (and then re-installs) it's own LLHooks during a sendInput command.

 

I hope someone finds this useful. I've been using this for the last month or so in application where I'm (physically) performing over 200 actions per minute and haven't noticed any problems or conflicts with the automations.

 

Cheers, RHCP.

 

 

Test Script:

Spoiler

 
Note: The functions which use post message do have a two naughty global variables GameIdentifier and classIdentifier - you have to set this to your window/control of of interest
 

The two commands you want to use for to post input are MTclick and pSend.
 

Spoiler


Grendahl
  • Members
  • 416 posts
  • Last active: Jul 07 2014 08:01 PM
  • Joined: 10 Aug 2009

nor are the automations themselves affected by user input.

 
Using Notepad to test this, I notice that the output to Notepad isn't "pure". 
 
What I mean by that, is that while I use your example to send a loop of 10 numbers to notepad, if I simultaneously type in another application, some of my keystrokes are getting injected into notepad along with the desired output.

Spoiler

That example yields something like this as output as I typed in the following:
so this is an example of the text that leaks into notepad while i'm typing in another window... so this is close, but not perfect.
 
1
2
 3
4
k5
t6
7
*
9
10
 
 
As you can see, some of the characters have "leaked" through. I'm also a bit confused as to how an asterisk found its way into Notepad, when I didn't type one?
 
 
Thoughts on how to obtain "cleaner" output in Notepad?


Always have your scripts when you need them with Dropbox.
Sign up for free! http://db.tt/9Hrieqj

RHCP
  • Members
  • 1228 posts
  • Last active: Apr 08 2017 06:17 PM
  • Joined: 29 May 2006

The problem there is that " Input.revertKeyState() " is posting the keydown messages to the wrong window i.e. notepad. This is because for my needs the user would always be using the same window that i'm automating the input with.

 

You could simply add something like find active window in the Input.releaseKeys(), which is then used as the target window for  Input.revertKeyState().

 

Here is the keybind used in the below video

*f1::
Loop 100
{
	input.hookBlock(True, True)
	upsequence := Input.releaseKeys()
	critical, 1000
	input.hookBlock(False, False)
	if upsequence
		DllCall("Sleep", Uint, 15) 
;	DllCall("Sleep", Uint, 2000) ; removed so it speeds things up
	pSend(A_Index . "`n") 
;	Input.revertKeyState() ; Disabled to prevent the issue
	critical, off ; added so the hooks wont get removed
	sleep 100  ; to slow it down a bit
}
SoundPlay, *-1
return

[weird this video should be embedded]

 

Also be careful with the amount of time spent in critical. I'm not 100% sure if critical, 1000 will actually cause the messages sent to the input hooks to be processed after 1000 ms - i haven't actually tested this. But it is definitely possible for windows to remove the hooks from the input chain if it's taking too long.

 

Hope this helps.

 

Edit: Also text fields are one of the areas where you probably don't want to use Input.revertKeyState(). As the act of releasing the key (input.releasekeys()) will already produce the character the user is trying to type.



Grendahl
  • Members
  • 416 posts
  • Last active: Jul 07 2014 08:01 PM
  • Joined: 10 Aug 2009

Works beautifully! Thank you for the quick fix!


Always have your scripts when you need them with Dropbox.
Sign up for free! http://db.tt/9Hrieqj

d7o1d1s0
  • Members
  • 1 posts
  • Last active: Sep 29 2013 09:08 AM
  • Joined: 29 Sep 2013

Apologies if this is the wrong place for noob questions. 

 

I'm trying to write my first ever AHK script, all I need is to click 3 points on my screen every 10 seconds, without my inputs being disrupted. I appreciate this is probably very basic but my first attempt didn't work and I'm having trouble identifying where I went wrong and how to fix it. I opened this thread because my mouse was jumping while the script was running.

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn  ; Enable warnings to assist with detecting common errors.
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.

Loop{
Click -1326, 920
Click -1326, 452
Click -686, 920
Sleep 10000
}

This is as far as I've got, if anyone could help I'd greatly appreciate it.

 

Thanks