Lua Scripting

Lua API Reference

Lua 5.4 runs inside 2LT. Scripts can read world state, draw on the overlay, send input, work with Pathing, and query objects or enemies from code.

Lua 5.4 stdlib base · math · string · table screen coords normalized 0–1 color format ARGB hex

Runtime

Available standard libraries

base, math, string, table. There is no io, os, coroutine, or package.

Color format

All color values are ARGB hex integers. In 0xFFFFA040, alpha is the first byte and the color is orange.

Screen coordinates

All screen positions are normalized game-window percentages. 0, 0 is top-left; 1, 1 is bottom-right of the game window.

Stop behavior

Blocking calls like sleep() and waits throw when Stop is clicked. Let that happen; put cleanup in on_stop.

Return convention

Most functions return ok, error. Check ok before using the result. When ok is false, error explains what failed.

Node indices

All path node indices are 1-based, matching standard Lua table conventions.

Utility

Status, waits, retries, cleanup, and key state.
Logging, status, and cleanup
Print progress and clean up when the script exits.
log(msg)
script_status(msg)
get_time()           -- ms since script start
script_should_stop() -- bool
on_stop(fn)          -- ok, error
script_status sets the live status line on the Scripts page. on_stop registers a callback that fires on any exit reason. The callback receives reason and an optional err string. Reason values: "complete", "error", "stop", "script_replacement". Multiple callbacks are stacked and run in reverse registration order.
Example
on_stop(function(reason, err)
    overlay_clear("debug")
    send_mouse_up(1)
    if reason == "error" then log(err) end
end)

script_status("Waiting for launch")
log("elapsed " .. get_time() .. "ms")
Waits and retries
Wait or retry without trapping the Stop button.
sleep(ms)
wait_until(predicate, timeout_ms?, interval_ms?) -- ok, error
retry(count, delay_ms, action)                  -- ok, error, attempts
wait_until polls a predicate every interval_ms until it returns truthy. The default interval is 25 ms and the clamp is 5–1000 ms. retry calls an action up to count times with delay_ms between attempts. Count is clamped 1–1000, delay 0–120000 ms.
Example
local ok, err = wait_until(function()
    return activity_get_state() == "ACTIVE"
end, 5000)

local ok, err, attempts = retry(3, 500, function(attempt)
    script_status("Attempt " .. attempt)
    return activity_get_state() == "ACTIVE", "not active yet"
end)
Key state
Read Windows virtual-key state from Lua.
is_key_down(vk_code)    -- bool
is_key_pressed(vk_code) -- bool, fires once per down transition
is_key_pressed fires once per down transition, useful for toggles. Use Windows virtual-key codes. F1–F12 are 0x700x7B.
Example
if is_key_pressed(0x70) then  -- F1
    log("toggled")
end

Activity

Activity state, waits, ID writes, and launch helpers.
Activity state reads
Read destination name, state string, and full metadata.
activity_get_name(timeout_ms?)   -- name, error
activity_get_state(timeout_ms?)  -- state, error
activity_get_info()              -- table
Activity state values: "ACTIVE", "setup:orbit", "cleanup", "NONE". activity_get_state is the simplest check for orbit, loading, and active-world transitions.
Example
local state, err = activity_get_state(1000)
if state then log("state: " .. state) end
Wait for activity state
Block until one or more specific states are reached.
activity_wait_state(state_or_states, timeout_ms?) -- ok, state_or_error
Pass a string for a single accepted state or a table of strings to accept any of several. Stop interrupts the wait. On timeout the second return value is an error string.
Example
local ok, state = activity_wait_state({ "ACTIVE", "setup:orbit" }, 15000)
if not ok then log(state) end
Set or launch activity
Set an activity ID or run the saved orbit launch click flow.
activity_set_id(id, timeout_ms?)  -- ok, error
activity_launch(id, timeout_ms?)  -- ok, error
activity_launch sets the activity ID, then uses the saved orbit launch clicks: open Director, select the Tower tab, click the Courtyard LZ, set the ID, and click Launch. It retries up to three times. Calibrate the launch positions on the Scripts tab first.
Example
local ok, err = activity_launch(123456, 30000)
if not ok then error(err) end

Features

Temporary feature changes and script-controlled values.
Feature toggles
Turn named 2LT features on or off from Lua.
feature_enable(name, timeout_ms?)             -- ok, error
feature_disable(name, timeout_ms?)            -- ok, error
feature_enable_for_script(name, timeout_ms?)  -- ok, error
feature_enable_for_script turns the feature off automatically when the script exits or is stopped, so you do not need cleanup code in on_stop.
Example
local ok, err = feature_enable_for_script("pve_esp")
if not ok then error(err) end
Feature values
Read or write feature values that scripts are allowed to change.
feature_get_value(name, timeout_ms?)                   -- value, error
feature_get_value_info(name, timeout_ms?)              -- info, error
feature_set_value(name, value, timeout_ms?)            -- ok, error
feature_set_value_for_script(name, value, timeout_ms?) -- ok, error
feature_set_value_for_script captures the original value and restores it automatically during script cleanup. Prefer this when the change is meant to be temporary.
Example
local old = feature_get_value("aim_fov")
feature_set_value_for_script("aim_fov", 10.0)

Player

Position reads, writes, and movement helpers.
World reads
Read player position, velocity, view angles, and direction vectors.
player_loaded()       -- bool
get_position()        -- x, y, z
get_velocity()        -- x, y, z
get_view_angles()     -- yaw, pitch
get_forward_vector()  -- x, y, z
get_right_vector()    -- x, y, z
get_up_vector()       -- x, y, z (always 0, 0, 1)
distance_to(x, y, z)  -- meters
Forward and right vectors are camera-relative and flattened onto the X/Y plane. Check player_loaded() before reads during long loading screens.
Example
local x, y, z = get_position()
log(string.format("pos %.1f %.1f %.1f  dist %.1f",
    x, y, z, distance_to(0, 0, 0)))
World writes
Write player position, velocity, or view angles directly.
set_position(x, y, z)
set_position_x(v), set_position_y(v), set_position_z(v)
set_velocity(x, y, z)
set_velocity_x(v), set_velocity_y(v), set_velocity_z(v)
set_view_angles(yaw, pitch)
set_position also writes a mini velocity of 0, 0, 0.1 alongside the position. This nudge is necessary because Destiny's physics engine only picks up the new position when velocity changes. Without it, the engine overwrites your write and the player does not actually move. Use go_to or path_run when you need blocking movement with arrival detection.
Example
local x, y, z = get_position()
set_position(x, y, z + 1.5)
set_velocity(0, 0, 0)
Movement helpers
Move relative to the camera or travel to a world point with arrival detection.
move_forward(distance, speed?)  -- blocking
move_right(distance, speed?)
move_up(distance, speed?)
go_to(x, y, z, options?)       -- ok, error
Relative helpers use velocity writes and block until the distance is covered or movement stalls for 3 s. Default speed is 10 m/s. go_to accepts { mode, speed, radius, timeout_ms }. Movement mode is "velocity" or "teleport" ("tp" is also accepted).
Example
local x, y, z = get_position()
local ok, err = go_to(x + 3, y, z, {
    mode = "velocity",
    speed = 8,
    radius = 0.5,
    timeout_ms = 5000
})

Input

Cursor math, calibrated clicks, action input, keys, and mouse.
Cursor and coordinate conversion
Read cursor and window state, then convert between pixels, screen percent, and world space.
cursor_get_position()                   -- x, y (desktop pixels)
window_get_size()                       -- width, height (pixels)
screen_to_desktop(x_pct, y_pct)        -- x, y
desktop_to_screen(x, y)                -- x_pct, y_pct
world_to_screen(x, y, z)               -- ok, x_pct, y_pct
cursor_set_position(x, y, timeout_ms?)  -- ok, error
world_to_screen returns false as its first value when the point is behind the camera. All returned percentages are in the 0–1 range.
Example
local x, y, z = get_position()
local ok, sx, sy = world_to_screen(x, y, z + 1)
if ok then
    draw_screen_marker(sx, sy, { color = 0xFF00FF00 })
end
Percent-based screen input
Move, click, or drag using game-window percentages.
move_cursor_screen(x_pct, y_pct, options?)              -- ok, error
click_screen(x_pct, y_pct, options?)                    -- ok, error
drag_screen(x1_pct, y1_pct, x2_pct, y2_pct, options?)  -- ok, error
input_do(action_name, timeout_ms?)                      -- ok, error
Use percent-based input in shared scripts so users do not need desktop pixel coordinates. Options include timeout_ms, button, hold_ms, and settle_ms. input_do resolves and fires a Destiny action binding by name.
Example
click_screen(0.88, 0.91, { timeout_ms = 12000 })
drag_screen(0.30, 0.40, 0.70, 0.40, { button = 1, hold_ms = 100 })
Destiny action input
Use the player's Destiny keybinds instead of hard-coded virtual keys.
send_input(action_name, hold_ms?)  -- ok, error
send_input_down(action_name)       -- ok, error
send_input_up(action_name)         -- ok, error
input_get_binding(action_name)     -- primary, secondary, error
input_reload_bindings()           -- ok, error
Action names come from Destiny's cvars.xml. Common examples: "fire", "reload", "interact", "move_forward", "ui_open_director". send_input_down actions are auto-released when the script stops; raw send_mouse_down / send_key_down calls must be paired with a release in on_stop.
Example
send_input("interact")

send_input_down("fire")
sleep(500)
send_input_up("fire")
Low-level key and mouse input
Send raw virtual-key or mouse button events.
send_key_down(vk),   send_key_up(vk)
send_key(vk, hold_ms?)                  -- blocking, interruptible
send_mouse_down(btn), send_mouse_up(btn)
send_click(btn, hold_ms?)               -- blocking, interruptible
Pair every raw down with an up. Mouse button 1 = left click, 2 = right click. send_key and send_click block for the hold duration and are interrupted by Stop. Prefer send_input for Destiny-bound actions so scripts work regardless of the player's keybinds.
Example
send_key(0x45)  -- E key

send_mouse_down(1)
on_stop(function() send_mouse_up(1) end)

Overlay

Screen and world drawing, persistent handles, markers, and cleanup.
Temporary overlay draws
Draw temporary world or screen visuals.
draw_world_text(text, x, y, z, options?)
draw_world_line(x1, y1, z1, x2, y2, z2, options?)
draw_world_cube(x, y, z, options?)
draw_world_marker(x, y, z, options?)
draw_screen_text(text, x_pct, y_pct, options?)
draw_screen_line(x1_pct, y1_pct, x2_pct, y2_pct, options?)
draw_screen_rect(x_pct, y_pct, w_pct, h_pct, options?)
draw_screen_marker(x_pct, y_pct, options?)
Common options: duration_ms, color, fill_color, thickness, size, shape, label, align, filled, layer. Colors are ARGB hex. Screen coordinates are normalized 0–1.
Example
draw_world_marker(x, y, z, {
    label = "target",
    shape = "cube",
    duration_ms = 2000,
    color = 0xFFFFA040
})
draw_screen_text("status", 0.5, 0.08, { align = "center" })
Persistent overlay handles
Create an overlay once, then update it each tick.
overlay_create_world_text(text, x, y, z, options?)          -- handle
overlay_update_world_text(handle, text, x, y, z, options?)
overlay_create_world_line(x1, y1, z1, x2, y2, z2, options?) -- handle
overlay_update_world_line(handle, x1, y1, z1, x2, y2, z2, options?)
overlay_create_world_cube(x, y, z, options?)                -- handle
overlay_update_world_cube(handle, x, y, z, options?)
overlay_create_screen_text(text, x_pct, y_pct, options?)    -- handle
overlay_update_screen_text(handle, text, x_pct, y_pct, options?)
overlay_create_screen_line(x1_pct, y1_pct, x2_pct, y2_pct, options?) -- handle
overlay_update_screen_line(handle, x1_pct, y1_pct, x2_pct, y2_pct, options?)
overlay_create_screen_rect(x_pct, y_pct, w_pct, h_pct, options?) -- handle
overlay_update_screen_rect(handle, x_pct, y_pct, w_pct, h_pct, options?)
Persistent handles ignore duration_ms and stay visible until overlay_remove, overlay_clear, or script cleanup. Omitting options on an update call preserves the current style.
Example
local h = overlay_create_screen_text("running", 0.5, 0.08, {
    layer = "hud",
    align = "center"
})
overlay_update_screen_text(h, "step 2", 0.5, 0.08)
overlay_remove(h)
Markers and cleanup
Persistent labeled points and cleanup helpers.
marker_create_world(x, y, z, options?)          -- handle
marker_update_world(handle, x, y, z, options?)  -- ok
marker_create_screen(x_pct, y_pct, options?)    -- handle
marker_update_screen(handle, x_pct, y_pct, options?) -- ok
marker_remove(handle)     -- ok
overlay_remove(handle)    -- ok, works on any persistent handle
overlay_clear(layer?)     -- count removed
overlay_remove works on any persistent handle including marker handles. overlay_clear with no argument clears all script-owned overlays. Register an on_stop callback to ensure overlays are removed when the script ends.
Example
local layer = "route"
local h = marker_create_world(x, y, z, { layer = layer, label = "start" })
on_stop(function() overlay_clear(layer) end)

Objects

SObjects, enemies, aim locks, and velocity locks.

Every game entity is an SObject: light sources, bullet holes, enemy weapons, projectiles, players, enemies, vehicles, and more. Each type has a type_id and subtype_id.

To find IDs for a specific entity, open the SObject Inspector in the ESP tab.

SObject queries
Find world objects by type, subtype, state, distance, and anchor point.
find_closest_sobject(type_id, max_distance?, include_state_ff?)
    -- found, x, y, z, distance, state_flag, subtype_id

find_closest_sobject_subtype(type_id, subtype_id, max_distance?, include_state_ff?)
    -- found, x, y, z, distance, state_flag, subtype_id

find_closest_sobject_filtered(filter)
    -- found, x, y, z, distance, state_flag, subtype_id

find_closest_sobject_near(type_id, x, y, z, radius, include_state_ff?, min_player_distance?)
    -- found, x, y, z, distance, state_flag, subtype_id

find_closest_sobject_near_subtype(type_id, subtype_id, x, y, z, radius, ...)
    -- found, x, y, z, distance, state_flag, subtype_id

wait_sobject(filter, timeout_ms?)
    -- object_table, error
SObjects are identified by type_id and subtype_id, each in range 0–255. wait_sobject blocks and returns a table with x, y, z, distance, type_id, subtype_id, state_flag, and address.

The state_flag (FF flag) marks motion. 2LT uses it to tell live enemies from dead bodies. Non-zero means the entity is moving; zero means it is still. By default, queries only return entities with a zero state flag. Pass include_state_ff = true to include both.
Filter table keys
{
    type_id = 14,
    subtype_id = 3,               -- optional
    radius = 20,
    near = { x=0, y=0, z=0 },   -- anchor; or x/y/z as root keys
    include_state_ff = true,
    min_player_distance = 0
}
Example
local obj, err = wait_sobject({
    type_id = 14,
    subtype_id = 3,
    radius = 20,
    include_state_ff = true
}, 2500)
if obj then
    draw_world_marker(obj.x, obj.y, obj.z, { label = "found" })
end
Object watches
Detect objects that appeared or changed after an action.
watch_sobjects_near(type_id, x, y, z, radius, include_state_ff?, min_player_distance?)
    -- watch_id
watch_sobjects_near_subtype(type_id, subtype_id, x, y, z, radius, ...)
    -- watch_id
watch_sobjects_near_filtered(filter)
    -- watch_id
find_new_sobject(watch_id, timeout_ms?)
    -- found, x, y, z, distance, state_flag, subtype_id

watch_sobject_transition_filtered(filter)
    -- watch_id
find_changed_sobject(watch_id, timeout_ms?)
    -- found, x, y, z, distance, state_flag, subtype_id
Call a watch function before your action to snapshot matching objects, then call find_new_sobject to find what appeared afterward. Use transition watches when an existing address changes type or subtype.
Example
local ex, ey, ez = get_position()
local watch = watch_sobjects_near_subtype(14, 3, ex, ey, ez, 20, true)
send_input("interact")
local found, x, y, z = find_new_sobject(watch, 2500)
if found then draw_world_marker(x, y, z) end
Enemies and aiming
Find resolved PvE enemies and aim at world positions or bones. ESP and aim features can use Visibility Check to ignore blocked targets.
find_closest_enemy(entity_id, max_distance?)          -- found, x, y, z, distance
wait_enemy_gone(entity_id, max_distance?, timeout_ms?) -- ok, error
aim_at(x, y, z)                                       -- bool
aim_at_closest_enemy(entity_id, max_distance?)        -- bool
Enemy entity IDs are 16-bit values (0–65535) shown by PvE ESP after resolving an enemy object. aim_at_closest_enemy resolves to the head bone position when available and falls back to the entity root. The built-in ESP and aim settings include Visibility Check for blocked targets. aim_at writes camera angles for any arbitrary world coordinate.
Example
if aim_at_closest_enemy(18041, 150) then
    send_click(1, 50)
end
wait_enemy_gone(18041, 150, 10000)
Background locks
Keep aim or velocity writes running while Lua waits or does other work.
-- Aim lock
aim_lock_start(entity_id, max_distance?, interval_ms?) -- ok, error
aim_lock_stop()                                        -- true
aim_lock_running()                                     -- bool
aim_lock_has_target()                                  -- bool
aim_lock_get_target()                                  -- found, x, y, z, distance

-- Velocity lock
velocity_lock_start(x, y, z, interval_ms?) -- ok, error
velocity_lock_set(x, y, z)                 -- ok, error (lock must be running)
velocity_lock_stop(stop_velocity?)         -- true
velocity_lock_running()                    -- bool
velocity_lock_get()                        -- running, x, y, z
Both locks run on a background worker at the given interval. Default is 16 ms and the clamp is 5–250 ms. They stop when the script exits or is stopped. Avoid using velocity_lock and path_run at the same time unless you mean to.
Example
local ok, err = aim_lock_start(18041, 150)
if not ok then error(err) end
send_input_down("fire")
wait_enemy_gone(18041, 150, 8000)
send_input_up("fire")
aim_lock_stop()

Pathing

Routes, node actions, settings, and path info.
Task-based path run
Build a route, attach node actions, and run it as a blocking script task.
path_clear()
path_add_node(x, y, z)
path_on_node(index, fn)  -- ok, error; fn receives node index
path_clear_actions()
path_run(options?)       -- ok, error
Node action callbacks receive the node index. They can sleep, wait, draw overlays, or inject input. path_run stops and returns an error string prefixed with the node index if an action fails. Options: mode, speed, radius, timeout_ms, start_index, end_index.
Example
path_clear()
path_add_node(100, 200, 50)
path_add_node(110, 210, 50)
path_on_node(1, function()
    send_input("interact")
    sleep(1000)
end)
local ok, err = path_run({ mode = "velocity", speed = 10 })
Path settings
Set movement mode, speed, loop behavior, and arrival radius.
path_set_move_mode(mode)           -- ok, error
path_get_move_mode()               -- mode string
path_set_speed(speed)
path_set_arrival_radius(radius)
path_set_loop(mode)
path_set_node_delay(index, seconds)
path_go_to_node(index, options?)   -- ok, error
Move mode accepts "velocity", "teleport", or "tp". Loop mode accepts "off", "loop", or "pingpong". All node indices are 1-based.
Example
path_set_move_mode("velocity")
path_set_speed(12)
path_set_arrival_radius(0.5)
path_set_loop("off")
Compatibility runner and callbacks
Use the older queued path runner for existing route scripts.
path_start()
path_start_from(index)
path_stop()
path_wait_complete(timeout_ms)
on_node_arrive(fn)  -- fn receives node index
Prefer path_run for new scripts. The compatibility runner is for older route scripts and paths started from the Pathing UI. on_node_arrive sets one global arrival callback.
Example
on_node_arrive(function(index)
    log("arrived at node " .. index)
end)
path_start_from(2)
path_wait_complete(0)  -- 0 = no timeout
Path persistence and info
Save routes, load routes, and inspect node data while a script runs.
path_save(name)                -- bool
path_load(name)                -- bool
path_get_node_count()          -- count
path_get_current_target()      -- index (1-based)
path_get_node_position(index)  -- x, y, z
path_get_node_label(index)     -- label string
path_get_node_delay(index)     -- seconds
path_is_running()              -- bool
path_set_draw_cubes(bool)
path_set_draw_lines(bool)
Paths are saved to %APPDATA%\2LT\paths\<name>.2ltp. Use Copy as Lua on the Pathing page to export the current route as a ready-to-run path_run script.
Example
path_load("myroute")
path_set_draw_cubes(true)
path_set_draw_lines(true)
log("nodes: " .. path_get_node_count())
local ok, err = path_run()