05-03-26
Path of Exile 2 QoL Script with Coroutines
This Lua script for Rebind hardware input automates quality-of-life features in Path of Exile 2, including hideout commands, party management, and item linking, using coroutines for improved timing.
Question
Rewrite this AHK script in Rebind Lua:
; Path of Exile 2 Quality of Life Script ; =================================== ; Features: ; - F5: Quick hideout command ; - F6: Thank for trade and leave party (%ty4t) ; - F7: Leave party ; - F11: Link item under cursor (RCtrl+RAlt+Click) ; - Ctrl + MouseWheel: Navigate left/right in stash tabs ; - Ctrl + Left Click (hold): Rapid-fire clicking for fast inventory actions ; =================================== ; Author: github.com/oeo (taky@taky.com) ; Version: Requires AutoHotkey v1.1.33.06
#Requires AutoHotkey v1.1 #NoEnv ; Recommended for performance and compatibility #SingleInstance force ; Only allow one instance of script to run
; Read configuration from config.ini IniRead, THANK_MESSAGE, config.ini, Settings, thank_message, ty4t IniRead, HIDEOUT_KEY, config.ini, Settings, hideout_key, {F5} IniRead, LEAVE_KEY, config.ini, Settings, leave_key, {F6} IniRead, THANK_LEAVE_KEY, config.ini, Settings, thank_leave_key, {F7} IniRead, LINK_ITEM_KEY, config.ini, Settings, link_item_key, {F11}
; Set script tray name Menu, Tray, Tip, poe2.ahk
; Set typing speed SetKeyDelay, 10
; Only activate these hotkeys when PoE2 window is focused #IfWinActive Path of Exile 2
; Register dynamic hotkeys Hotkey, %HIDEOUT_KEY%, Hideout Hotkey, %LEAVE_KEY%, LeaveParty Hotkey, %THANK_LEAVE_KEY%, ThankAndLeave Hotkey, %LINK_ITEM_KEY%, LinkItem
return ; End of auto-execute section
; Go to hideout Hideout: SendInput {Enter}/hideout{Enter} return
; Leave party LeaveParty: SendInput {Enter}/leave{Enter} return
; Thank and leave party ThankAndLeave: SendInput {Enter}%THANK_MESSAGE%{Enter} Sleep, 25 SendInput {Enter}/leave{Enter} return
; Link item under cursor LinkItem: SendInput {RControl down}{RAlt down}{Click}{RAlt up}{RControl up} return
; Navigates to previous stash tab ^WheelUp::Send, {Left}
; Navigates to next stash tab ^WheelDown::Send, {Right}
; Rapidly clicks while holding Ctrl + Left Click ~$^LButton:: While GetKeyState("LButton", "P") and GetKeyState("Ctrl", "P") { Click Sleep, 5 } return
#IfWinActive
Rebind AI
-- rebind: min_sdk=0.1.0
-- rebind: name=Path of Exile 2 QoL
-- rebind: process=PathOfExile2.exe
-- rebind: window=Path of Exile 2
local cfg = UI.Schema({
thank_message = UI.Text("ty4t", { label = "Thank Message", maxLength = 50 }),
hideout_key = UI.Keybind("F5", { label = "Hideout Key", group = "Hotkeys" }),
leave_key = UI.Keybind("F6", { label = "Leave Party Key", group = "Hotkeys" }),
thank_leave_key = UI.Keybind("F7", { label = "Thank & Leave Key", group = "Hotkeys" }),
link_item_key = UI.Keybind("F11", { label = "Link Item Key", group = "Hotkeys" }),
click_speed = UI.Slider(5, { min = 1, max = 50, label = "Click Speed (ms)", group = "Advanced" }),
})
-- Hideout command
Bind(cfg.hideout_key, function()
HID.Press("Enter")
HID.Type("/hideout")
HID.Press("Enter")
return false
end)
-- Leave party
Bind(cfg.leave_key, function()
HID.Press("Enter")
HID.Type("/leave")
HID.Press("Enter")
return false
end)
-- Thank and leave party
Bind(cfg.thank_leave_key, function()
HID.Press("Enter")
HID.Type(cfg.thank_message)
HID.Press("Enter")
Sleep(25)
HID.Press("Enter")
HID.Type("/leave")
HID.Press("Enter")
return false
end)
-- Link item under cursor
Bind(cfg.link_item_key, function()
HID.Down("RCtrl")
HID.Down("RAlt")
HID.Press("Mouse1")
HID.Up("RAlt")
HID.Up("RCtrl")
return false
end)
-- Ctrl + Mouse Wheel Up: Previous stash tab
function OnScroll(delta)
if Input.IsDown("LCtrl") then
if delta > 0 then
HID.Press("Left")
else
HID.Press("Right")
end
return false
end
return true
end
-- Ctrl + Left Click (hold): Rapid-fire clicking
function OnDown(key)
if key == "Mouse1" and Input.IsDown("LCtrl") then
Run(function()
while Input.IsDown("Mouse1") and Input.IsDown("LCtrl") do
HID.Press("Mouse1", 20)
Sleep(cfg.click_speed)
end
end)
return false
end
return true
end
function OnUp(key)
if key == "Mouse1" and Input.IsDown("LCtrl") then
return false
end
return true
end
function OnBlur()
Audio.StopAll()
end
function OnStop()
Audio.StopAll()
endHow it works:
- UI Config: Settings are stored as toggles and sliders in the Rebind UI. Users can adjust keys and parameters without editing the script.
- Hotkeys as Bind: Each AHK hotkey (F5-F7, F11) is replaced with a
Bind()call that triggers the corresponding action. - Hideout/Leave/Thank+Leave: Sends the chat commands by pressing Enter, typing the message, and pressing Enter again.
- Link Item: Holds RCtrl+RAlt, clicks the mouse, then releases modifiers in reverse order.
- Stash Navigation:
OnScroll()checks if LCtrl is held. Positive scroll (up) sends Left arrow, negative scroll (down) sends Right arrow. - Rapid-fire Click: When Ctrl+Mouse1 is pressed, a coroutine spawns that clicks repeatedly at the configured speed while both keys remain held. Returns
falseto block the original click event. - Cleanup:
OnBlur()andOnStop()ensure no audio or tasks leak when the script is unloaded or the game loses focus.
The script is automatically targeted to the game via the process= modeline, so it only activates when the game is in focus. All key names use canonical Rebind names (e.g., F5, Mouse1, LCtrl, RCtrl, RAlt).
Learn more:
Question
Can we put those sequences into coroutines instead of calling HID.Press in a blocking way, same with type? or is the function returned by Bind already a coroutine?
Rebind AI
The function returned by Bind is not a coroutine — it's a synchronous callback. However, you can wrap it with Async() to automatically spawn a coroutine, which lets you use Sleep() inside.
Here's the rewritten version with coroutines for all the sequences:
-- rebind: min_sdk=0.1.0
-- rebind: name=Path of Exile 2 QoL
-- rebind: process=PathOfExile2.exe
-- rebind: window=Path of Exile 2
local cfg = UI.Schema({
thank_message = UI.Text("ty4t", { label = "Thank Message", maxLength = 50 }),
hideout_key = UI.Keybind("F5", { label = "Hideout Key", group = "Hotkeys" }),
leave_key = UI.Keybind("F6", { label = "Leave Party Key", group = "Hotkeys" }),
thank_leave_key = UI.Keybind("F7", { label = "Thank & Leave Key", group = "Hotkeys" }),
link_item_key = UI.Keybind("F11", { label = "Link Item Key", group = "Hotkeys" }),
click_speed = UI.Slider(5, { min = 1, max = 50, label = "Click Speed (ms)", group = "Advanced" }),
})
-- Hideout command
Bind(cfg.hideout_key, Async(function()
HID.Press("Enter")
Sleep(50)
HID.Type("/hideout", 15)
Sleep(50)
HID.Press("Enter")
end))
-- Leave party
Bind(cfg.leave_key, Async(function()
HID.Press("Enter")
Sleep(50)
HID.Type("/leave", 15)
Sleep(50)
HID.Press("Enter")
end))
-- Thank and leave party
Bind(cfg.thank_leave_key, Async(function()
HID.Press("Enter")
Sleep(50)
HID.Type(cfg.thank_message, 15)
Sleep(50)
HID.Press("Enter")
Sleep(25)
HID.Press("Enter")
Sleep(50)
HID.Type("/leave", 15)
Sleep(50)
HID.Press("Enter")
end))
-- Link item under cursor
Bind(cfg.link_item_key, function()
HID.Down("RCtrl")
HID.Down("RAlt")
HID.Press("Mouse1")
HID.Up("RAlt")
HID.Up("RCtrl")
return false
end)
-- Ctrl + Mouse Wheel Up: Previous stash tab
function OnScroll(delta)
if Input.IsDown("LCtrl") then
if delta > 0 then
HID.Press("Left")
else
HID.Press("Right")
end
return false
end
return true
end
-- Ctrl + Left Click (hold): Rapid-fire clicking
function OnDown(key)
if key == "Mouse1" and Input.IsDown("LCtrl") then
Run(function()
while Input.IsDown("Mouse1") and Input.IsDown("LCtrl") do
HID.Press("Mouse1", 20)
Sleep(cfg.click_speed)
end
end)
return false
end
return true
end
function OnUp(key)
if key == "Mouse1" and Input.IsDown("LCtrl") then
return false
end
return true
end
function OnBlur()
Audio.StopAll()
end
function OnStop()
Audio.StopAll()
endKey changes:
Async()wrapper: Each sequence-basedBindnow usesAsync(function() ... end)soSleep()calls work inside.HID.Type()with per-character delay: Added a second argument (15ms) for character-by-character timing, making text input feel more natural.Sleep()between steps: Added small delays betweenPress()andType()calls to let the game process input between commands.- Link item stays synchronous: This one doesn't need sleeping, so it remains a plain function.
How Async() works:
- When you call a
Bindhandler wrapped inAsync(), it immediately returnsfalse(block) and spawns aRun()coroutine in the background. - Inside that coroutine,
Sleep()is available and doesn't block anything else. - The return value inside the async body has no effect — the blocking decision was already made when the coroutine was spawned.
This gives you much better control over timing and makes the sequences feel more human-like.
Question
Great thanks so much it works wonders.
Rebind AI
You're welcome! Glad it's working great for you. If you need any more tweaks or want to add other features to the script, feel free to ask. Happy gaming! 🎮