Lua API Reference
2LT exposes a Lua 5.4 scripting environment with direct access to world state, overlay drawing, input injection, the Pathing system, and full object/enemy queries.
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. 0xFFFFA040 is fully opaque orange — alpha occupies the high byte.
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
sleep() and every blocking wait throw when the user clicks Stop. Do not catch this — use on_stop for cleanup instead.
Return convention
Most functions return ok, error. Check ok first; error is a string describing the failure when ok is false.
Node indices
All path node indices are 1-based, matching standard Lua table conventions.
Utility
Status, waits, retry, cleanup, and key state.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.
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")
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 (default 25, clamp 5–1000) until truthy. retry calls an action up to count times with delay_ms between attempts; the action receives the attempt number and should return a truthy first value on success plus an optional error string on failure. Count is clamped 1–1000, delay 0–120000 ms.
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)
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 0x70–0x7B.
if is_key_pressed(0x70) then -- F1 log("toggled") end
Activity
Activity reads, waits, ID writes, and the launch flow.activity_get_name(timeout_ms?) -- name, error activity_get_state(timeout_ms?) -- state, error activity_get_info() -- table
"ACTIVE", "setup:orbit", "cleanup", "NONE". activity_get_state is the simplest check for orbit, loading, and active-world transitions.
local state, err = activity_get_state(1000) if state then log("state: " .. state) end
activity_wait_state(state_or_states, timeout_ms?) -- ok, state_or_error
local ok, state = activity_wait_state({ "ACTIVE", "setup:orbit" }, 15000) if not ok then log(state) end
activity_set_id(id, timeout_ms?) -- ok, error activity_launch(id, timeout_ms?) -- ok, error
activity_launch sets the activity ID then runs the calibrated orbit launch click sequence: opens the Director, selects the Tower tab, clicks the Courtyard LZ, sets the ID, and clicks Launch. Retries up to three times. Calibrate the launch positions on the Scripts tab first.
local ok, err = activity_launch(123456, 30000) if not ok then error(err) end
Features
Temporary feature setup and scriptable values.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 queues an automatic disable when the script exits or is stopped, so no cleanup code is needed in on_stop.
local ok, err = feature_enable_for_script("pve_esp") if not ok then error(err) end
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.
local old = feature_get_value("aim_fov") feature_set_value_for_script("aim_fov", 10.0)
Player
World reads, writes, and movement helpers.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
player_loaded() before reads during long loading screens.
local x, y, z = get_position() log(string.format("pos %.1f %.1f %.1f dist %.1f", x, y, z, distance_to(0, 0, 0)))
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 doesn't actually move. Use go_to or path_run when you need blocking movement with arrival detection.
local x, y, z = get_position() set_position(x, y, z + 1.5) set_velocity(0, 0, 0)
move_forward(distance, speed?) -- blocking move_right(distance, speed?) move_up(distance, speed?) go_to(x, y, z, options?) -- ok, error
go_to accepts { mode, speed, radius, timeout_ms }. Movement mode is "velocity" or "teleport" ("tp" is also accepted).
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
Coordinates, calibrated clicks, action input, keys, and mouse.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.
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
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
timeout_ms, button, hold_ms, and settle_ms. input_do resolves and fires a Destiny action binding by name.
click_screen(0.88, 0.91, { timeout_ms = 12000 }) drag_screen(0.30, 0.40, 0.70, 0.40, { button = 1, hold_ms = 100 })
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
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.
send_input("interact") send_input_down("fire") sleep(500) send_input_up("fire")
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
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.
send_key(0x45) -- E key send_mouse_down(1) on_stop(function() send_mouse_up(1) end)
Overlay
Screen/world drawing, persistent handles, markers, and cleanup.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?)
duration_ms, color, fill_color, thickness, size, shape, label, align, filled, layer. Colors are ARGB hex. Screen coordinates are normalized 0–1.
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" })
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?)
duration_ms and stay visible until overlay_remove, overlay_clear, or script cleanup. Omitting options on an update call preserves the current style.
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)
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.
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, aiming, aim locks, and velocity locks.Every entity in the game is an SObject — light sources, bullet holes, enemy weapons, projectiles, players, enemies, vehicles, and more. Each type is identified by a type_id and subtype_id.
To find the IDs for a specific entity, use the SObject Inspector tool in the ESP tab of the 2LT menu.
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
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) is an in-motion flag — 2LT uses it to differentiate live enemies from dead bodies. A non-zero value means the entity is in motion; zero means it is still. By default queries only return entities with a zero state flag. Pass include_state_ff = true to include entities regardless of their motion state.
{
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
}
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
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
find_new_sobject to detect objects that appeared after the snapshot. Use transition watches when an existing address changes type or subtype rather than a brand-new address spawning.
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
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
aim_at_closest_enemy resolves to the head bone position when available and falls back to the entity root. aim_at writes camera angles for any arbitrary world coordinate.
if aim_at_closest_enemy(18041, 150) then send_click(1, 50) end wait_enemy_gone(18041, 150, 10000)
-- 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
velocity_lock and path_run simultaneously unless intentional.
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, path settings, and path info.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
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.
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_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
"velocity", "teleport", or "tp". Loop mode accepts "off", "loop", or "pingpong". All node indices are 1-based.
path_set_move_mode("velocity") path_set_speed(12) path_set_arrival_radius(0.5) path_set_loop("off")
path_start() path_start_from(index) path_stop() path_wait_complete(timeout_ms) on_node_arrive(fn) -- fn receives node index
path_run for new scripts. The compatibility runner is useful for existing route scripts and for paths started from the Pathing UI. on_node_arrive sets a single global arrival callback.
on_node_arrive(function(index) log("arrived at node " .. index) end) path_start_from(2) path_wait_complete(0) -- 0 = no timeout
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)
%APPDATA%\2LT\paths\<name>.2ltp. Use the Pathing page's Copy as Lua button to export the current route as a ready-to-run path_run script.
path_load("myroute") path_set_draw_cubes(true) path_set_draw_lines(true) log("nodes: " .. path_get_node_count()) local ok, err = path_run()