MouseGetPos and other methods of retrieving mouse axis input are fundamentally flawed when it comes to games, because many games mess with the mouse cursor (Keep on moving it to middle of screen etc) or other limitations (eg when the cursor reaches the edge of the screen, you cannot generate any more input in that direction)
Games that do not operate by mouse cursor (eg FPS) generally read the mouse by "Delta" information - that is, the amount that it moved in each direction since the last update.
This library uses the RawInput API to read these Delta Move packets.
Note: If you wish to write scripts that send mouse movement at the kinds of rates that real mice operate at, see my LLMouse library.
================================== LIBRARY ========================================
Code: Select all
; Instantiate this class and pass it a func name or a Function Object
; The specified function will be called with the delta move for the X and Y axes
; Normally, there is no windows message "mouse stopped", so one is simulated.
; After 10ms of no mouse movement, the callback is called with 0 for X and Y
Class MouseDelta {
State := 0
__New(callback){
;~ this.TimeoutFn := this.TimeoutFunc.Bind(this)
this.MouseMovedFn := this.MouseMoved.Bind(this)
this.Callback := callback
}
Start(){
static DevSize := 8 + A_PtrSize, RIDEV_INPUTSINK := 0x00000100
; Register mouse for WM_INPUT messages.
VarSetCapacity(RAWINPUTDEVICE, DevSize)
NumPut(1, RAWINPUTDEVICE, 0, "UShort")
NumPut(2, RAWINPUTDEVICE, 2, "UShort")
NumPut(RIDEV_INPUTSINK, RAWINPUTDEVICE, 4, "Uint")
; WM_INPUT needs a hwnd to route to, so get the hwnd of the AHK Gui.
; It doesn't matter if the GUI is showing, it still exists
Gui +hwndhwnd
NumPut(hwnd, RAWINPUTDEVICE, 8, "Uint")
this.RAWINPUTDEVICE := RAWINPUTDEVICE
DllCall("RegisterRawInputDevices", "Ptr", &RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize )
OnMessage(0x00FF, this.MouseMovedFn)
this.State := 1
return this ; allow chaining
}
Stop(){
static RIDEV_REMOVE := 0x00000001
static DevSize := 8 + A_PtrSize
OnMessage(0x00FF, this.MouseMovedFn, 0)
RAWINPUTDEVICE := this.RAWINPUTDEVICE
NumPut(RIDEV_REMOVE, RAWINPUTDEVICE, 4, "Uint")
DllCall("RegisterRawInputDevices", "Ptr", &RAWINPUTDEVICE, "UInt", 1, "UInt", DevSize )
this.State := 0
return this ; allow chaining
}
SetState(state){
if (state && !this.State)
this.Start()
else if (!state && this.State)
this.Stop()
return this ; allow chaining
}
Delete(){
this.Stop()
;~ this.TimeoutFn := ""
this.MouseMovedFn := ""
}
; Called when the mouse moved.
; Messages tend to contain small (+/- 1) movements, and happen frequently (~20ms)
MouseMoved(wParam, lParam){
Critical
; RawInput statics
static DeviceSize := 2 * A_PtrSize, iSize := 0, sz := 0, pcbSize:=8+2*A_PtrSize, offsets := {x: (20+A_PtrSize*2), y: (24+A_PtrSize*2)}, uRawInput
static axes := {x: 1, y: 2}
; Get hDevice from RAWINPUTHEADER to identify which mouse this data came from
VarSetCapacity(header, pcbSize, 0)
If (!DllCall("GetRawInputData", "UPtr", lParam, "uint", 0x10000005, "UPtr", &header, "Uint*", pcbSize, "Uint", pcbSize) or ErrorLevel)
Return 0
ThisMouse := NumGet(header, 8, "UPtr")
; Find size of rawinput data - only needs to be run the first time.
if (!iSize){
r := DllCall("GetRawInputData", "UInt", lParam, "UInt", 0x10000003, "Ptr", 0, "UInt*", iSize, "UInt", 8 + (A_PtrSize * 2))
VarSetCapacity(uRawInput, iSize)
}
sz := iSize ; param gets overwritten with # of bytes output, so preserve iSize
; Get RawInput data
r := DllCall("GetRawInputData", "UInt", lParam, "UInt", 0x10000003, "Ptr", &uRawInput, "UInt*", sz, "UInt", 8 + (A_PtrSize * 2))
x := 0, y := 0 ; Ensure we always report a number for an axis. Needed?
x := NumGet(&uRawInput, offsets.x, "Int")
y := NumGet(&uRawInput, offsets.y, "Int")
this.Callback.(ThisMouse, x, y)
;~ ; There is no message for "Stopped", so simulate one
;~ fn := this.TimeoutFn
;~ SetTimer, % fn, -50
}
;~ TimeoutFunc(){
;~ this.Callback.("", 0, 0)
;~ }
}
A diagnostic utility to show you the input that comes from a mouse, in real-time
Code: Select all
#include MouseDelta.ahk
#SingleInstance,Force
Gui, Add, ListBox, w300 h200 hwndhOutput
Gui, Add, Text, xm w300 center, Hit F12 to toggle on / off
Gui, Show,, Mouse Watcher
MacroOn := 0
md := new MouseDelta("MouseEvent")
return
GuiClose:
md.Delete()
md := ""
ExitApp
F12::
MacroOn := !MacroOn
md.SetState(MacroOn)
return
; Gets called when mouse moves
; x and y are DELTA moves (Amount moved since last message), NOT coordinates.
MouseEvent(MouseID, x := 0, y := 0){
global hOutput
static text := ""
static LastTime := 0
t := A_TickCount
text := "x: " x ", y: " y (LastTime ? (", Delta Time: " t - LastTime " ms, MouseID: " MouseID) : "")
GuiControl, , % hOutput, % text
sendmessage, 0x115, 7, 0,, % "ahk_id " hOutput
LastTime := t
}
An example of usage that can map mouse axes to WSAD or Arrow keys is included.
If you wish to map mouse to Joystick, see my cVJoyinterface library on GitHub (Link in Sig).
There is also a complete Mouse to Joystick script in the Oneswitch-Utilities repo.
Code: Select all
#SingleInstance force
#include MouseDelta.ahk
MacroOn := 0
md := new MouseDelta("MouseEvent")
return
F12::
MacroOn := !MacroOn
md.SetState(MacroOn)
return
; Keeps a track of a "Pool" for each direction of movement (u, d, l, r)
; The pools constantly empty every 10ms at the rate dictated by DecayRate
; Moving the mouse in a direction will add the delta value to the pool, to a max of PoolCap
; If one direction's pool crosses the threshold, that direction is considered "Held"
; In this case, the opposite direction's pool is emptied.
MouseEvent(MouseID, x := "", y := ""){
; User configurables
static DecayRate := 1 ; The rate at which the pools decay
static PoolCap := 20 ; The highest value a pool can hold
static Threshold := 3 ; If a pool crosses this value, it is considered held
; Output keys
static KeyMap := {y: {-1: "Up", 1: "Down"}, x: {-1: "Left", 1: "Right"}} ; Arrow Keys
; End of user configurables
; StopFns are fired while the pool for an exis is not empty
static StopFns := {x: Func("MouseEvent").Bind(-1,0), y: Func("MouseEvent").Bind(-1,"",0)}
static VectorPools := {x: {-1: 0, 1: 0}, y: {-1: 0, 1: 0}}
Static AxisStates := {x: 0, y: 0}
static Vectors := {-1: "", 1: ""} ; just used for iteration
static TimerStates := {x: 0, y: 0}
input_data := {x: x, y: y}
; Apply current input to the pools
for axis, value in input_data {
if (value == "")
continue
; Deplete vector pools
for v, u in vectors {
VectorPools[axis, v] -= DecayRate
if (VectorPools[axis, v] < 0)
VectorPools[axis, v] := 0
}
; Update pool states
vector := GetVector(value)
fn := StopFns[axis]
if (vector){
; Movement for this axis
magnitude := abs(value)
VectorPools[axis, vector] += magnitude
if (VectorPools[axis, vector] > PoolCap){
VectorPools[axis, vector] := PoolCap
}
is_over_thresh := (VectorPools[axis, vector] > Threshold)
was_over_thresh := (VectorPools[axis, vector] > Threshold)
if (!was_over_thresh && is_over_thresh){
; Crossed threshold, cancel out opposite axis
; This is used to allow switching of directions relatively quickly.
; If we cross the threshold on a new vector, zero out the other vector
VectorPools[axis, vector * -1] := 0
}
; If there was movement on this axis and the timer is not running, then start it running
if (!TimerStates[axis]){
SetTimer, % fn, 10
}
} else {
; No movement for this axis
if (VectorPools[axis, -1] + VectorPools[axis, 1] == 0){
; Pools for this axis are empty, stop timer
SetTimer, % fn, Off
}
}
}
; Change states of outputs according to pools
for axis, value in input_data {
new_vector := (VectorPools[axis, -1] > Threshold ? -1 : (VectorPools[axis, 1] > Threshold ? 1 : 0))
if (new_vector != AxisStates[axis]){
; This axis changed state
if (AxisStates[axis] != 0){
; One of the directions for this axis was previously held, release old key
Send % "{" KeyMap[axis, AxisStates[axis]] " up}"
}
if (new_vector){
; The new state is non-zero, so press the new key
Send % "{" KeyMap[axis, new_vector] " down}"
}
}
AxisStates[axis] := new_vector
}
}
; Returns -1 for negative input, +1 for positive, else 0
GetVector(val){
if (val == 0)
return 0
else if (val < 0)
return -1
else
return 1
}
How to implement a "Sniper Mode" with any mouse.
When sniper mode is OFF (Default), it multiplies mouse movement by the value you set in ScaleFactor.
When you turn it off, it stops amplifying movement.
You will likely need to turn the mouse sensitivity down in the game to compensate.
Code: Select all
#SingleInstance force
#include MouseDelta.ahk
; ============= START USER-CONFIGURABLE SECTION =============
ShiftKey := "F12" ; The key used to shift DPI. Can be any key name from the AHK Key list: https://autohotkey.com/docs/KeyList.htm
ShiftMode := 1 ; 0 for shift-while-held, 1 for toggle
ScaleFactor := 4 ; The amount to multiply movement by when not in Sniper Mode
; ============= END USER-CONFIGURABLE SECTION =============
; Adjust ScaleFactor. If the user wants 2x sensitivity, we only need to send 1x input...
; ... because the user already moved the mouse once, so we only need to send that input 1x more...
; ... to achieve 2x sensitivity
ScaleFactor -= 1
SniperMode := 0
md := new MouseDelta("MouseEvent").Start()
hotkey, % ShiftKey, ShiftPressed
if (!ShiftMode){
hotkey, % ShiftKey " up", ShiftReleased
}
return
ShiftPressed:
if (ShiftMode){
SniperMode := !SniperMode
} else {
SniperMode := 1
}
md.SetState(!SniperMode)
return
ShiftReleased:
if (!ShiftMode){
SniperMode := 0
}
md.SetState(!SniperMode)
return
; Gets called when mouse moves or stops
; x and y are DELTA moves (Amount moved since last message), NOT coordinates.
MouseEvent(MouseID, x := 0, y := 0){
global ScaleFactor
if (MouseID){
x *= ScaleFactor, y *= ScaleFactor
DllCall("mouse_event",uint,1,int, x ,int, y,uint,0,int,0)
}
}