Skip to content

Instantly share code, notes, and snippets.

@peteristhegreat
Created January 3, 2026 00:31
Show Gist options
  • Select an option

  • Save peteristhegreat/c44c66127d0f0ff08724e5dc781326c0 to your computer and use it in GitHub Desktop.

Select an option

Save peteristhegreat/c44c66127d0f0ff08724e5dc781326c0 to your computer and use it in GitHub Desktop.
3D Maze Navigation with XBox Controller, Windows, Daedalus, AHK v2, XInput

Daedalus

https://github.com/CruiserOne/Daedalus

http://www.astrolog.org/labyrnth/daedalus.htm

Daedalus is super powerful. It has all sorts of neat features. If you do file open, you can open one of many ds files

$ ls -1 *.ds
carletn1.ds
carletn2.ds
demos.ds
dragon.ds
gigamaze.ds
glacier.ds
gripsox.ds
hunger.ds
jareth.ds
mandy.ds
maze4d.ds
maze5d.ds
mousemaz.ds
pacman.ds
pentris.ds
safari.ds
sokoban.ds
squared.ds
stocker.ds
survmaz0.ds
survmaz1.ds
survmaz2.ds
survmaz3.ds
survmaz4.ds
survmaz5.ds
survmaz6.ds
survmaz7.ds
survmaz8.ds
survmaz9.ds
wordmaze.ds

Safari and Glacier are the ones I've tried a few times.

Following the help and info menus, you can use the arrow keys and e and some of the function keys to navigate them, unless you are running as admin. Oh, and you can use shift + arrows to strafe.

AHK + XInput

https://www.autohotkey.com/boards/viewtopic.php?style=19&f=83&t=106254

https://autohotkey.wiki/guides:joysticks

Welp, to navigate with a controller is more fun. I had used a setup like this before and it wasn't too hard to get it working again.

Download the two ahk files on this page and put them in your documents folder.

Download and install AHK v2 (or higher?). Run on Windows.

Note: the script runs in the system tray. Find the Green H icon if you want to quit or close the script.

#Requires AutoHotkey v2.0
; Daedalus (Walter Pollen) - Xbox controller -> keyboard (AHK v2)
#SingleInstance Force
SendMode "Input"
SetWorkingDir A_ScriptDir
SetKeyDelay(-1, -1)
#Include <XInput>
XInput_Init()
; --------- CONFIG ----------
userIndex := 0 ; 0-based XInput controller index (0..3)
deadZoneL := XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE ; 7849 default
deadZoneR := XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE ; 8689 default
pollMs := 10 ; polling interval
; State tracking to avoid spamming key events
kUp := 0, kDown := 0, kLeft := 0, kRight := 0 ; combined arrows from DPad + sticks
rShift := 0
btnX := 0, btnA := 0, btnStart := 0
btnLBumper := 0, btnRBumper := 0
SetTimer(PollController, pollMs)
PollController() {
global userIndex, deadZoneL, deadZoneR
global kUp, kDown, kLeft, kRight, rShift
global btnX, btnA, btnStart, btnLBumper, btnRBumper
global XINPUT_GAMEPAD_DPAD_UP, XINPUT_GAMEPAD_DPAD_RIGHT, XINPUT_GAMEPAD_DPAD_DOWN, XINPUT_GAMEPAD_DPAD_LEFT
global XINPUT_GAMEPAD_X, XINPUT_GAMEPAD_A, XINPUT_GAMEPAD_START, XINPUT_GAMEPAD_LEFT_SHOULDER, XINPUT_GAMEPAD_RIGHT_SHOULDER
if !(state := XInput_GetState(userIndex))
return ; controller not connected
buttons := state.wButtons
; inside PollController(), after you read state:
; ToolTip "LX=" state.sThumbLX " LY=" state.sThumbLY "`nRX=" state.sThumbRX " RY=" state.sThumbRY
; --- Buttons ---
SendKeyStateChange("e", !!(buttons & XINPUT_GAMEPAD_X), btnX), btnX := !!(buttons & XINPUT_GAMEPAD_X) ; X -> "e"
SendKeyStateChange("Space", !!(buttons & XINPUT_GAMEPAD_A), btnA), btnA := !!(buttons & XINPUT_GAMEPAD_A) ; A -> Space
SendKeyStateChange("F1", !!(buttons & XINPUT_GAMEPAD_START), btnStart), btnStart := !!(buttons & XINPUT_GAMEPAD_START) ; Start -> F1
SendKeyStateChange("F5", !!(buttons & XINPUT_GAMEPAD_LEFT_SHOULDER), btnLBumper), btnLBumper := !!(buttons & XINPUT_GAMEPAD_LEFT_SHOULDER)
SendKeyStateChange("F6", !!(buttons & XINPUT_GAMEPAD_RIGHT_SHOULDER), btnRBumper), btnRBumper := !!(buttons & XINPUT_GAMEPAD_RIGHT_SHOULDER)
; --- Arrows from DPad + Left stick + Right stick (with Shift) ---
dUp := !!(buttons & XINPUT_GAMEPAD_DPAD_UP)
dRight := !!(buttons & XINPUT_GAMEPAD_DPAD_RIGHT)
dDown := !!(buttons & XINPUT_GAMEPAD_DPAD_DOWN)
dLeft := !!(buttons & XINPUT_GAMEPAD_DPAD_LEFT)
lx := state.sThumbRX, ly := state.sThumbRY
ll := lx < -deadZoneR*2, lr := lx > deadZoneR*2
lu := ly > deadZoneR, ld := ly < -deadZoneR ; Y axis is inverted (up is positive)
; Right stick -> Shift+arrows
rx := state.sThumbLX, ry := state.sThumbLY
rl := rx < -deadZoneL, rr := rx > deadZoneL
ru := ry > deadZoneL, rd := ry < -deadZoneL
anyR := rl || rr || ru || rd
if anyR && !rShift {
SendInput "{Shift down}"
rShift := 1
} else if !anyR && rShift {
SendInput "{Shift up}"
rShift := 0
}
upState := dUp || lu || (rShift && ru)
rightState := dRight || lr || (rShift && rr)
downState := dDown || ld || (rShift && rd)
leftState := dLeft || ll || (rShift && rl)
SendKeyStateChange("Up", upState, kUp), kUp := upState
SendKeyStateChange("Right", rightState, kRight), kRight := rightState
SendKeyStateChange("Down", downState, kDown), kDown := downState
SendKeyStateChange("Left", leftState, kLeft), kLeft := leftState
}
; Helper: only sends Down/Up transitions
SendKeyStateChange(key, newState, oldState) {
if newState == oldState
return
SendInput "{" key (newState ? " down" : " up") "}"
}
/* XInput by Lexikos
*/
#Requires AutoHotkey v2.0-beta.6
/*
Function: XInput_Init
Initializes XInput.ahk with the given XInput DLL.
Parameters:
dll - The path or name of the XInput DLL to load.
*/
XInput_Init(dll := "") {
global
if _XInput_hm ?? 0
return
;======== CONSTANTS DEFINED IN XINPUT.H ========
; Device types available in XINPUT_CAPABILITIES
XINPUT_DEVTYPE_GAMEPAD := 0x01
; Device subtypes available in XINPUT_CAPABILITIES
XINPUT_DEVSUBTYPE_GAMEPAD := 0x01
; Flags for XINPUT_CAPABILITIES
XINPUT_CAPS_VOICE_SUPPORTED := 0x0004
; Constants for gamepad buttons
XINPUT_GAMEPAD_DPAD_UP := 0x0001
XINPUT_GAMEPAD_DPAD_DOWN := 0x0002
XINPUT_GAMEPAD_DPAD_LEFT := 0x0004
XINPUT_GAMEPAD_DPAD_RIGHT := 0x0008
XINPUT_GAMEPAD_START := 0x0010
XINPUT_GAMEPAD_BACK := 0x0020
XINPUT_GAMEPAD_LEFT_THUMB := 0x0040
XINPUT_GAMEPAD_RIGHT_THUMB := 0x0080
XINPUT_GAMEPAD_LEFT_SHOULDER := 0x0100
XINPUT_GAMEPAD_RIGHT_SHOULDER := 0x0200
XINPUT_GAMEPAD_GUIDE := 0x0400
XINPUT_GAMEPAD_A := 0x1000
XINPUT_GAMEPAD_B := 0x2000
XINPUT_GAMEPAD_X := 0x4000
XINPUT_GAMEPAD_Y := 0x8000
; Gamepad thresholds
XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE := 7849
XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE := 8689
XINPUT_GAMEPAD_TRIGGER_THRESHOLD := 30
; Flags to pass to XInputGetCapabilities
XINPUT_FLAG_GAMEPAD := 0x00000001
;=============== END CONSTANTS =================
if (dll = "")
loop files A_WinDir "\System32\XInput1_*.dll"
dll := A_LoopFileName
if (dll = "")
dll := "XInput1_3.dll"
_XInput_hm := DllCall("LoadLibrary", "str", dll, "ptr")
if !_XInput_hm
throw Error("Failed to initialize XInput: " dll " not found.")
(_XInput_GetState := DllCall("GetProcAddress", "ptr", _XInput_hm, "ptr", 100, "ptr"))
|| (_XInput_GetState := DllCall("GetProcAddress", "ptr", _XInput_hm, "astr", "XInputGetState", "ptr"))
_XInput_SetState := DllCall("GetProcAddress", "ptr", _XInput_hm, "astr", "XInputSetState", "ptr")
_XInput_GetCapabilities := DllCall("GetProcAddress", "ptr", _XInput_hm, "astr", "XInputGetCapabilities", "ptr")
if !(_XInput_GetState && _XInput_SetState && _XInput_GetCapabilities) {
XInput_Term()
throw Error("Failed to initialize XInput: function not found.")
}
}
/*
Function: XInput_GetState
Retrieves the current state of the specified controller.
Parameters:
UserIndex - [in] Index of the user's controller. Can be a value from 0 to 3.
Returns:
An object with with properties equivalent to merging XINPUT_STATE and XINPUT_GAMEPAD.
https://docs.microsoft.com/en-us/windows/win32/api/xinput/ns-xinput-xinput_state
Error handling:
Returns false if the controller is disconnected.
Throws an OSError for other errors.
*/
XInput_GetState(UserIndex) {
global _XInput_GetState
xiState := Buffer(16)
if err := DllCall(_XInput_GetState, "uint", UserIndex, "ptr", xiState) {
if err = 1167 ; ERROR_DEVICE_NOT_CONNECTED
return 0
throw OSError(err, -1)
}
return {
dwPacketNumber: NumGet(xiState, 0, "UInt"),
wButtons: NumGet(xiState, 4, "UShort"),
bLeftTrigger: NumGet(xiState, 6, "UChar"),
bRightTrigger: NumGet(xiState, 7, "UChar"),
sThumbLX: NumGet(xiState, 8, "Short"),
sThumbLY: NumGet(xiState, 10, "Short"),
sThumbRX: NumGet(xiState, 12, "Short"),
sThumbRY: NumGet(xiState, 14, "Short"),
}
}
/*
Function: XInput_SetState
Sends data to a connected controller. This function is used to activate the vibration
function of a controller.
Parameters:
UserIndex - [in] Index of the user's controller. Can be a value from 0 to 3.
LeftMotorSpeed - [in] Speed of the left motor, between 0 and 65535.
RightMotorSpeed - [in] Speed of the right motor, between 0 and 65535.
Error handling:
Throws an OSError on failure, such as if the controller is not connected.
Remarks:
The left motor is the low-frequency rumble motor. The right motor is the
high-frequency rumble motor. The two motors are not the same, and they create
different vibration effects.
*/
XInput_SetState(UserIndex, LeftMotorSpeed, RightMotorSpeed) {
global _XInput_SetState
if err := DllCall(_XInput_SetState, "uint", UserIndex, "uint*", LeftMotorSpeed | RightMotorSpeed << 16)
throw OSError(err, -1)
}
/*
Function: XInput_GetCapabilities
Retrieves the capabilities and features of a connected controller.
Parameters:
UserIndex - [in] Index of the user's controller. Can be a value in the range 0–3.
Flags - [in] Input flags that identify the controller type.
0 - All controllers.
1 - XINPUT_FLAG_GAMEPAD: Xbox 360 Controllers only.
Returns:
An object with properties equivalent to XINPUT_CAPABILITIES.
https://docs.microsoft.com/en-au/windows/win32/api/xinput/ns-xinput-xinput_capabilities
Error handling:
Returns false if the controller is disconnected.
Throws an OSError for other errors.
*/
XInput_GetCapabilities(UserIndex, Flags) {
global _XInput_GetCapabilities
xiCaps := Buffer(20)
if err := DllCall(_XInput_GetCapabilities, "uint", UserIndex, "uint", Flags, "ptr", xiCaps) {
if err = 1167 ; ERROR_DEVICE_NOT_CONNECTED
return 0
throw OSError(err, -1)
}
return {
Type: NumGet(xiCaps, 0, "UChar"),
SubType: NumGet(xiCaps, 1, "UChar"),
Flags: NumGet(xiCaps, 2, "UShort"),
Gamepad: {
wButtons: NumGet(xiCaps, 4, "UShort"),
bLeftTrigger: NumGet(xiCaps, 6, "UChar"),
bRightTrigger: NumGet(xiCaps, 7, "UChar"),
sThumbLX: NumGet(xiCaps, 8, "Short"),
sThumbLY: NumGet(xiCaps, 10, "Short"),
sThumbRX: NumGet(xiCaps, 12, "Short"),
sThumbRY: NumGet(xiCaps, 14, "Short")
},
Vibration: {
wLeftMotorSpeed: NumGet(xiCaps, 16, "UShort"),
wRightMotorSpeed: NumGet(xiCaps, 18, "UShort")
}
}
}
/*
Function: XInput_Term
Unloads the previously loaded XInput DLL.
*/
XInput_Term() {
global
if _XInput_hm
DllCall("FreeLibrary", "uint", _XInput_hm), _XInput_hm := _XInput_GetState := _XInput_SetState :=
_XInput_GetCapabilities := 0
}
; TODO: XInputEnable, 'GetBatteryInformation and 'GetKeystroke.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment