Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Delayed Actions

The Delayed Actions API provides a comprehensive timer system for executing code after delays, with full control over timing, pausing, cancellation, and looping. This system is similar to Unreal Engine’s timer system.

Overview

All delayed actions execute on the game thread and return handles that can be used to control them. Actions are owned by the mod that created them, preventing cross-mod interference.

Creating Delayed Actions

ExecuteInGameThreadWithDelay

Executes a callback after a specified delay.

Overload 1: Auto-generated handle

local handle = ExecuteInGameThreadWithDelay(delayMs, callback)
#TypeInformation
1integerDelay in milliseconds before executing
2functionCallback to execute
ReturnintegerHandle for the created action

Overload 2: User-provided handle (UE Delay-style)

ExecuteInGameThreadWithDelay(handle, delayMs, callback)
#TypeInformation
1integerHandle (from MakeActionHandle())
2integerDelay in milliseconds
3functionCallback to execute

This overload only creates the action if the handle is not already active. This mirrors Unreal Engine’s Delay node behavior.

RetriggerableExecuteInGameThreadWithDelay

Executes a callback after a delay, but resets the timer if called again with the same handle.

RetriggerableExecuteInGameThreadWithDelay(handle, delayMs, callback)
#TypeInformation
1integerHandle (from MakeActionHandle())
2integerDelay in milliseconds
3functionCallback to execute

This is useful for debouncing - each call resets the timer, so the callback only fires after the delay with no new calls.

LoopInGameThreadWithDelay

Creates a repeating timer that executes the callback at regular intervals.

local handle = LoopInGameThreadWithDelay(delayMs, callback)
#TypeInformation
1integerDelay in milliseconds between executions
2functionCallback to execute
ReturnintegerHandle for the created loop

The loop continues until cancelled with CancelDelayedAction(handle).

ExecuteInGameThreadAfterFrames

Executes a callback after a specified number of frames.

local handle = ExecuteInGameThreadAfterFrames(frames, callback)
#TypeInformation
1integerNumber of frames to wait
2functionCallback to execute
ReturnintegerHandle for the created action

Note: Requires EngineTick hook. Check EngineTickAvailable before using.

LoopInGameThreadAfterFrames

Creates a repeating timer that executes every N frames.

local handle = LoopInGameThreadAfterFrames(frames, callback)
#TypeInformation
1integerNumber of frames between executions
2functionCallback to execute
ReturnintegerHandle for the created loop

Note: Requires EngineTick hook. Check EngineTickAvailable before using.

MakeActionHandle

Generates a unique handle for use with delay functions.

local handle = MakeActionHandle()

| Return | integer | A unique handle that can be used with delay functions |

Controlling Delayed Actions

CancelDelayedAction

Cancels a pending or active delayed action.

local success = CancelDelayedAction(handle)
#TypeInformation
1integerHandle of the action to cancel
ReturnbooleanTrue if the action was found and cancelled

PauseDelayedAction

Pauses a delayed action, preserving remaining time.

local success = PauseDelayedAction(handle)
#TypeInformation
1integerHandle of the action to pause
ReturnbooleanTrue if the action was found and paused

UnpauseDelayedAction

Resumes a paused delayed action.

local success = UnpauseDelayedAction(handle)
#TypeInformation
1integerHandle of the action to unpause
ReturnbooleanTrue if the action was found and unpaused

ResetDelayedActionTimer

Restarts the timer with its original delay.

local success = ResetDelayedActionTimer(handle)
#TypeInformation
1integerHandle of the action to reset
ReturnbooleanTrue if the action was found and reset

SetDelayedActionTimer

Changes the delay and restarts the timer.

local success = SetDelayedActionTimer(handle, newDelayMs)
#TypeInformation
1integerHandle of the action to modify
2integerNew delay in milliseconds
ReturnbooleanTrue if the action was found and modified

ClearAllDelayedActions

Cancels all delayed actions belonging to the current mod.

local count = ClearAllDelayedActions()

| Return | integer | Number of actions that were cancelled |

Note: This only affects actions created by the calling mod. Other mods’ actions are not affected.

Querying Delayed Actions

IsValidDelayedActionHandle

Checks if a handle refers to an existing, non-cancelled action.

local valid = IsValidDelayedActionHandle(handle)

IsDelayedActionActive

Checks if an action is currently active (not paused, not pending removal).

local active = IsDelayedActionActive(handle)

IsDelayedActionPaused

Checks if an action is currently paused.

local paused = IsDelayedActionPaused(handle)

GetDelayedActionRate

Gets the configured delay for an action.

local delayMs = GetDelayedActionRate(handle)

Returns configured delay in milliseconds for time-based actions, or frames for frame-based actions. Returns -1 if handle is invalid.

GetDelayedActionTimeRemaining

Gets the remaining time until the action fires.

local remainingMs = GetDelayedActionTimeRemaining(handle)

Returns remaining milliseconds for time-based actions, or remaining frames for frame-based actions. Returns -1 if handle is invalid.

GetDelayedActionTimeElapsed

Gets the time elapsed since the action was started/reset.

local elapsedMs = GetDelayedActionTimeElapsed(handle)

Returns elapsed milliseconds for time-based actions, or elapsed frames for frame-based actions. Returns -1 if handle is invalid.

Global Variables

EngineTickAvailable

Boolean indicating if the EngineTick hook is available.

if EngineTickAvailable then
    -- Frame-based delays are available
end

ProcessEventAvailable

Boolean indicating if the ProcessEvent hook is available.

if ProcessEventAvailable then
    -- ProcessEvent-based execution is available
end

Examples

Simple Delay

ExecuteInGameThreadWithDelay(2000, function()
    print("This prints after 2 seconds\n")
end)

Self-Cancelling Loop

local counter = 0
local loopHandle  -- Declare first for closure capture
loopHandle = LoopInGameThreadWithDelay(1000, function()
    counter = counter + 1
    print(string.format("Tick %d\n", counter))
    if counter >= 5 then
        CancelDelayedAction(loopHandle)
        print("Loop stopped\n")
    end
end)

Debounced Action

local debounceHandle = MakeActionHandle()

RegisterKeyBind(Key.F, function()
    -- Only fires 500ms after the last key press
    RetriggerableExecuteInGameThreadWithDelay(debounceHandle, 500, function()
        print("Debounced action fired\n")
    end)
end)

Pausable Timer

local timerHandle = ExecuteInGameThreadWithDelay(10000, function()
    print("Timer completed\n")
end)

-- Pause after 2 seconds
ExecuteInGameThreadWithDelay(2000, function()
    PauseDelayedAction(timerHandle)
    print("Timer paused\n")
end)

-- Resume after 5 seconds
ExecuteInGameThreadWithDelay(5000, function()
    UnpauseDelayedAction(timerHandle)
    print("Timer resumed\n")
end)

UE-Style Delay (Only Create If Not Exists)

local cooldownHandle = MakeActionHandle()

RegisterKeyBind(Key.E, function()
    -- Only creates the delay if not already active
    ExecuteInGameThreadWithDelay(cooldownHandle, 1000, function()
        print("Ability ready!\n")
    end)
    print("Ability used (1s cooldown)\n")
end)

Frame-Based Delay

if EngineTickAvailable then
    ExecuteInGameThreadAfterFrames(60, function()
        print("Fired after 60 frames\n")
    end)
end

Important Notes

  1. Closure Capture: When using loop handles inside their own callbacks, declare the variable before assignment:

    local loopHandle  -- Declare first
    loopHandle = LoopInGameThreadWithDelay(1000, function()
        CancelDelayedAction(loopHandle)  -- Now correctly captured
    end)
    
  2. Mod Ownership: Each mod can only control its own delayed actions. CancelDelayedAction, PauseDelayedAction, etc. will fail if called with a handle from another mod.

  3. Frame-Based Delays: Require the EngineTick hook. Always check EngineTickAvailable before using ExecuteInGameThreadAfterFrames or LoopInGameThreadAfterFrames.

  4. Fallback Behavior: If the default execution method is unavailable, the system will automatically fall back to the other method if available.