Skip to content

Instantly share code, notes, and snippets.

@RJNY
Last active February 15, 2026 00:11
Show Gist options
  • Select an option

  • Save RJNY/517f580b927762ff306303e66a562bb8 to your computer and use it in GitHub Desktop.

Select an option

Save RJNY/517f580b927762ff306303e66a562bb8 to your computer and use it in GitHub Desktop.

Apollo & Artemis - Pause / Resume scripts refactor

Reworked the pause/resume scripts for Apollo/Artemis. Both scripts are still standalone, just drop them anywhere and point Apollo's Command Preparations at them.

Disclaimer

Important

This is a personal refactor I'm sharing as-is. I'm not maintaining this, not accepting issues, not fielding feature requests. If it works for you, great. If not, the original gist is your best bet.

Warning

I'm figuring this out as I go. I've read through the original scripts, understood what they're doing, and made what I believe are improvements, but I haven't battle-tested these in every scenario Apollo can throw at them. The original scripts from the gist have been used by more people than these have (which is zero, at time of writing). Use these at your own risk, and if something breaks, the original scripts are right there to fall back to.

How does this compare to the original script?

Race condition

The big one. The original scripts assume pause always fires before resume, but Apollo doesn't guarantee that. When events come out of order, resume creates the lock file, then pause starts up and deletes it thinking it's stale. Game stays frozen.

Flipped the protocol so pause writes the PID into the lock file and resume reads it back, resumes the process directly, and deletes the file. If there's no lock file when resume runs, it just exits.

Permissions

Swapped PROCESS_ALL_ACCESS for PROCESS_SUSPEND_RESUME (0x0800). Only permission actually needed. Full access can get denied on some processes, which might be behind some of the "suspend stopped working" reports on the gist.

Error handling

The original helpers call MsgBox + ExitApp on failure, which triggers the cleanup handler, which calls ResumeProcess again. Now the helper just returns a boolean and the caller deals with it.

DllCall dedup

SuspendProcess and ResumeProcess were the same function with one string swapped. Merged them into CallNtProcessFunc(pid, funcName). Both scripts carry their own copy since Apollo invokes them as separate processes - pause.ahk on client disconnect, resume.ahk on reconnect (or as a global undo). They coordinate through the lock file: pause writes the game's PID into it, resume reads it back out, resumes the process, and deletes the file to signal pause that it can exit.

Also fixed a handle leak in here. The original code doesn't close the process handle if GetModuleHandle or GetProcAddress fails. OS cleans it up anyway on exit, but still.

Other stuff

  • Process name lookup uses "ahk_pid " pid explicitly instead of relying on AHK's "last found window" which can go stale.
  • Added steam.exe to the exclusion list per gist comments about Big Picture getting frozen.
  • Cleanup handler deletes the lock file on unexpected exit so the next run starts clean.
#Requires AutoHotkey v2.0
#SingleInstance Force
DetectHiddenWindows true
; ---------------------------------------------------------------------------
; pause.ahk - Suspend the active window's process.
;
; Triggered by Apollo's "Client Disconnect" Command Preparation.
; Writes the suspended PID to the lock file so resume.ahk can act
; independently, even if this script is no longer running.
; ---------------------------------------------------------------------------
global LockFile := A_Temp "\AutoPauseResume.lock"
global IsProcessSuspended := false
global IsProcessResumed := false
OnExit(Cleanup)
; Clean up any stale lock file from a previous run
if FileExist(LockFile)
FileDelete(LockFile)
; ---------------------------------------------------------------------------
; Excluded processes - add entries in lowercase
; ---------------------------------------------------------------------------
excluded := Map(
"explorer.exe", true,
"csrss.exe", true,
"winlogon.exe", true,
"svchost.exe", true,
"dwm.exe", true,
"cmd.exe", true,
"powershell.exe", true,
"chrome.exe", true,
"steam.exe", true,
"sublime_text.exe", true,
"sublime_merge.exe", true,
"steamwebhelper.exe", true,
"openconsole.exe", true,
"windowsterminal.exe", true,
"playnite.desktopapp.exe", true,
"playnite.fullscreenapp.exe", true
)
; ---------------------------------------------------------------------------
; Get the active window's PID and process name
; ---------------------------------------------------------------------------
if !WinExist("A")
{
MsgBox "No active window detected. Please focus a window to suspend."
ExitApp
}
pid := WinGetPID("A")
if !pid
{
MsgBox "Failed to get the active window's PID."
ExitApp
}
processName := WinGetProcessName("ahk_pid " pid)
if !processName
{
MsgBox "Failed to retrieve the active window's process name."
ExitApp
}
if excluded.Has(StrLower(processName))
ExitApp
; ---------------------------------------------------------------------------
; Suspend the process and write PID to lock file for resume.ahk
; ---------------------------------------------------------------------------
if !CallNtProcessFunc(pid, "NtSuspendProcess")
{
MsgBox "Failed to suspend process '" processName "' (PID: " pid ")."
ExitApp
}
IsProcessSuspended := true
FileAppend(pid, LockFile)
; Poll for lock file deletion (resume.ahk deletes it after resuming)
SetTimer(CheckResumed, 500)
return
; ---------------------------------------------------------------------------
CheckResumed(*)
{
global IsProcessResumed
if !FileExist(LockFile)
{
IsProcessResumed := true
ExitApp
}
}
; ---------------------------------------------------------------------------
; Safety net: if this script exits while the process is still suspended,
; resume it automatically.
; ---------------------------------------------------------------------------
Cleanup(exitReason, exitCode)
{
global pid, IsProcessSuspended, IsProcessResumed
if IsProcessSuspended && !IsProcessResumed
{
CallNtProcessFunc(pid, "NtResumeProcess")
if FileExist(LockFile)
FileDelete(LockFile)
}
}
; ---------------------------------------------------------------------------
; Calls NtSuspendProcess or NtResumeProcess on a PID.
; Uses PROCESS_SUSPEND_RESUME (0x0800) - the minimum required access right.
; Returns true on success, false on failure.
; ---------------------------------------------------------------------------
CallNtProcessFunc(pid, funcName)
{
hProc := DllCall("OpenProcess", "UInt", 0x0800, "Int", 0, "UInt", pid, "Ptr")
if !hProc
return false
hNtdll := DllCall("GetModuleHandle", "Str", "ntdll.dll", "Ptr")
if !hNtdll
{
DllCall("CloseHandle", "Ptr", hProc)
return false
}
pFunc := DllCall("GetProcAddress", "Ptr", hNtdll, "AStr", funcName, "Ptr")
if !pFunc
{
DllCall("CloseHandle", "Ptr", hProc)
return false
}
DllCall(pFunc, "Ptr", hProc)
DllCall("CloseHandle", "Ptr", hProc)
return true
}
#Requires AutoHotkey v2.0
#SingleInstance Force
; ---------------------------------------------------------------------------
; resume.ahk - Resume a previously suspended process.
;
; Triggered by Apollo's "Global Undo" or "Client Reconnect" Command
; Preparation. Reads the PID from the lock file and resumes the process
; directly, then deletes the lock file. Self-sufficient: works whether
; pause.ahk is still running or not. If resume fires before pause
; (no lock file), it's a clean no-op.
; ---------------------------------------------------------------------------
LockFile := A_Temp "\AutoPauseResume.lock"
if !FileExist(LockFile)
ExitApp
content := Trim(FileRead(LockFile))
if !content
{
FileDelete(LockFile)
ExitApp
}
pid := Integer(content)
CallNtProcessFunc(pid, "NtResumeProcess")
FileDelete(LockFile)
ExitApp
; ---------------------------------------------------------------------------
; Calls NtSuspendProcess or NtResumeProcess on a PID.
; Uses PROCESS_SUSPEND_RESUME (0x0800) - the minimum required access right.
; Returns true on success, false on failure.
; ---------------------------------------------------------------------------
CallNtProcessFunc(pid, funcName)
{
hProc := DllCall("OpenProcess", "UInt", 0x0800, "Int", 0, "UInt", pid, "Ptr")
if !hProc
return false
hNtdll := DllCall("GetModuleHandle", "Str", "ntdll.dll", "Ptr")
if !hNtdll
{
DllCall("CloseHandle", "Ptr", hProc)
return false
}
pFunc := DllCall("GetProcAddress", "Ptr", hNtdll, "AStr", funcName, "Ptr")
if !pFunc
{
DllCall("CloseHandle", "Ptr", hProc)
return false
}
DllCall(pFunc, "Ptr", hProc)
DllCall("CloseHandle", "Ptr", hProc)
return true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment