Skip to content

Globals

Top-level functions and small namespaces (storage) callable from any script. These don't need a prefix beyond the namespace name.

Four more namespaces live elsewhere: entities in Entities, and aim, trace and internal in the Internal API Reference.

Player Access

FunctionReturnsDescription
local_player()playerThe local player object
get_players()tableAll enemy players. Teammates are always available via get_teammates()
get_all_players()tableAll valid players except the local player
get_teammates()tableTeammate players only
lua
local me = local_player()
for _, p in ipairs(get_players()) do
    if p:is_enemy() and p:is_alive() then
        -- ...
    end
end

World Entities

FunctionReturnsDescription
get_world_entities([type])WorldEntity[]All tracked world entities. Optional type string filters by coarse type name (e.g. "sinner", "tower") or exact engine class name (e.g. "citadel_breakableprop")

Each WorldEntity has:

FieldTypeDescription
typestringCoarse type name ("sinner", "tower", "trooper_boss", "unknown", ...)
class_namestringExact engine class name
x, y, znumberWorld position
health, max_healthnumberCurrent and maximum health
teamintegerTeam id
distancenumberDistance from the local player, in metres (already converted)
subclassintegerm_nSubclassID. Separates subtypes that share one class_name, e.g. golden statue vs soul crate
currency_valueintegerSoul value of the pickup; 0 for non-soul rows
activebooleanWhether the pickup is available (false while on respawn cooldown)
pickup_namestringName of the powerup. Key is absent (nil) when empty, so test if e.pickup_name then
lua
-- Print all sinners (neutral soul-denying enemies)
for _, e in ipairs(get_world_entities("sinner")) do
    print(string.format("sinner at %.0f,%.0f hp=%d/%d", e.x, e.y, e.health, e.max_health))
end

-- Check for breakable props regardless of sub-type
for _, e in ipairs(get_world_entities("citadel_breakableprop")) do
    print(e.class_name, e.subclass)
end

Time

FunctionReturnsDescription
clock()numberHigh-resolution time in seconds. Use for deltatime and debouncing
game_time()numberMatch clock time in seconds (pause-aware HUD timer)
get_match_time()numberAlias for game_time()
is_paused()booleanMatch clock is frozen (a team pause, or the 3s unpause countdown)
sim_time()numberRaw client SimTime. Keeps advancing through pauses and pre-game
game.tick_count()integer or nilThe game's current tick number. nil, never 0, until the game clock is running
game.latency()number or nilNetwork latency in milliseconds. nil until a source serves it

clock() is wall-clock monotonic seconds since the engine started. game_time() (and its alias get_match_time()) advances with the match clock and freezes when the game is paused.

sim_time() is distinct from both. It is the raw client SimTime and keeps running through pauses and pre-game. game_time() freezes whenever is_paused() is true.

Do not diff absolute timestamps against sim_time()

The game's absolute timestamps are in the match-clock domain: cast_delay_start, cooldown_start / cooldown_end, channel_start, cast_completed, and buff expires_at. They freeze during pauses, so subtracting them from sim_time() over-counts by all the accumulated pause and pre-game time. In a real paused match that error measured about 126 seconds.

For "how much is left", use the pre-computed fields ability.cooldown and buff.remaining. Both are already pause-correct. Use sim_time() only for deltas between two sim_time() samples.

lua
-- Correct: read the pre-computed remaining time (already pause-aware)
local ab = local_player():get_ability(slot.ability1)
if ab and ab.cooldown > 0 then
    print(string.format("Ability 1: %.1fs left", ab.cooldown))
end

-- Correct: sim_time is fine for a delta between two sim_time samples
local started = sim_time()
-- ... some ticks later ...
local elapsed = sim_time() - started

game.tick_count() is the game's tick number. It is nil for about the first second of a match and while the match is paused, never 0. Game ticks are not the same rate as on_tick.

game.latency() returns nil until a source serves it, and none does yet.

Output

FunctionReturnsDescription
print(...)-Output to the app console, the Windows console, and the client's tsuki.log. Prefixed [script_name] in the app console and the log file; [Lua:script_name] on the Windows console. Arguments are stringified and tab-joined, as in stock Lua
toast(text, [duration], [style])-Centered HUD toast. Optional per-call styling. See below
notify(text, [duration], [color])-Persistent notification. Default duration 3.0, color white
play_sound(filename)-Play a WAV file from assets\sounds\. Pass the filename only, no path separators
lua
toast("ready")
notify("warning", 5.0)
play_sound("alert.wav")   -- plays assets\sounds\alert.wav

print also reaches the log file, so script output survives the session. After the fact you can tell "the script isn't running" from "the script is running quietly". Log lines look like [2026-08-23 21:04:11.882] [LUA] [my_script] ready.

toast(text, [duration], [style])

A centered HUD toast notification.

  • duration, seconds to display. Pass 0 to use the global default from menu settings.
  • style, optional table with per-call overrides. Any key omitted falls back to the global defaults configured in the menu:
KeyTypeNotes
bg{r, g, b, a}Background fill
text{r, g, b, a}Text color
outline{r, g, b, a}Outline / drop-shadow color
scalenumberSize multiplier (1.0 = default)

Toasts deduplicate by text only. If the same string is currently showing or already queued, the call is dropped on the floor, even if you passed a different style table. This means it's safe to fire toast("X") inside on_tick without flooding the screen; a tick-rate spam of the same message renders the original once.

lua
toast("Hit", 2, {bg = draw.color(255,0,0)})  -- shows
toast("Hit", 2, {bg = draw.color(0,255,0)})  -- DEDUPED (same text)
toast("Hit!", 2)                               -- shows (different text)

Different toasts with different text aren't deduplicated against each other, only repeats of the same string are suppressed, so a stream of "Hit X", "Hit Y", "Hit Z" all appear. The queue holds at most 5 pending toasts at a time.

lua
toast("Target locked", 2, {
    bg = {r = 40, g = 15, b = 50, a = 220},
    text = {r = 200, g = 140, b = 255},
    scale = 1.2
})

Demo script - each button triggers a different style preset:

lua
-- Toast Style Demo
-- Click buttons to see different toast styles
name        = "Toast Style Demo"
description = "Showcases custom toast styling"

ui.new_tab("toast_demo", "Toast Demo")

ui.button("toast_demo", "styles", "Default Toast", function()
    toast("Default style, uses your global settings", 3)
end)

ui.button("toast_demo", "styles", "Success (Green)", function()
    toast("Parry successful!", 3, {
        bg   = {r=20, g=60, b=20, a=220},
        text = {r=100, g=255, b=100},
        outline = {r=80, g=200, b=80, a=120},
    })
end)

ui.button("toast_demo", "styles", "Danger (Red)", function()
    toast("LOW HP, retreat!", 4, {
        bg   = {r=80, g=10, b=10, a=230},
        text = {r=255, g=80, b=80},
        outline = {r=255, g=60, b=60, a=150},
        scale = 1.3,
    })
end)

ui.button("toast_demo", "styles", "Info (Blue)", function()
    toast("Cooldown ready", 2, {
        bg   = {r=15, g=25, b=60, a=220},
        text = {r=120, g=180, b=255},
        outline = {r=80, g=140, b=220, a=100},
    })
end)

ui.button("toast_demo", "styles", "Big Gold", function()
    toast("ULTRA KILL!", 5, {
        bg   = {r=40, g=30, b=5, a=240},
        text = {r=255, g=215, b=0},
        outline = {r=255, g=180, b=0, a=160},
        scale = 1.8,
    })
end)

notify(text, [duration], [color])

A persistent notification. Default duration 3.0, color white.

  • color must be a packed integer - what draw.color(r, g, b, [a]) returns.
  • Unlike toast, notify does not accept an {r, g, b, a} table. A table, or a float such as 3.0, is silently ignored: the notification falls back to white, and nothing is raised.
lua
notify("boss up", 5.0, draw.color(255, 80, 80))   -- correct
notify("boss up", 5.0, { r = 255, g = 80, b = 80 })  -- ignored, shows white

Input shortcuts

FunctionReturnsDescription
slot_to_key(slot)integerVK code for an ability, item, jump, slide or weapon_melee slot. Returns 0, not nil, for any other slot. See Game Keybinds
item_slot_to_key(config_slot)integerVK code for a 0-based item config slot (0-3). Equivalent to slot_to_key(config_slot + 4)
left_click()-Simulate a left mouse click
right_click()-Simulate a right mouse click

For finer-grained input control (key down, key up, mouse movement), see the Input namespace.

Game state

is_position_visible(pos)

Performs a BVH raycast from the camera to a world-space position. Returns true if the path is unobstructed (the point is visible from the camera).

lua
local pos = some_player:get_position()
if is_position_visible(pos) then
    -- position has line-of-sight from camera
end

This is a point-in-world check, not a player-visibility check. For per-player visibility use player:is_visible() instead.

is_in_menu()

Returns true when the game cursor is visible, meaning the player is in a menu, shop, scoreboard, or settings screen.

lua
if is_in_menu() then return end

All input actions (key presses, mouse movement) from Lua scripts are automatically blocked while in menu. You don't need to check is_in_menu() for safety, it's handled internally. Use this to skip expensive logic like targeting when the player isn't actively playing.

is_game_focused()

Returns true when the game window is in the foreground (active and focused).

lua
if not is_game_focused() then return end

Use this to skip logic when the player has alt-tabbed or is in another window. Unlike is_in_menu(), this detects when the game itself isn't the active window.

Spectators

The game namespace exposes who is currently spectating the match.

FunctionReturnsDescription
game.get_spectators()tableArray of spectator name strings currently watching the match
game.get_spectator_count()integerNumber of spectators, without building the name table
lua
if game.get_spectator_count() > 0 then
    local specs = game.get_spectators()
    print(#specs .. " spectators: " .. table.concat(specs, ", "))
end

Tsuki Config

Read/write TSUKI's own settings (ESP toggles, aimbot FOV, etc.).

FunctionReturnsDescription
tsuki.get_bool(key, default)booleanRead a boolean setting
tsuki.get_int(key, default)integerRead an integer setting
tsuki.get_float(key, default)numberRead a float setting
tsuki.set_bool(key, value)-Write a boolean setting
tsuki.set_int(key, value)-Write an integer setting
tsuki.set_float(key, value)-Write a float setting
tsuki.get_color(key, default)ColorRead a color {r,g,b,a} (0-255)
tsuki.set_color(key, value)-Write a color {r,g,b,a} (0-255)
lua
-- React to the user's configured aimbot FOV
local fov = tsuki.get_float("aimbot_fov", 3.0)
local target = targeting.find_closest_by_fov(fov, 30.0)

-- Temporarily override a setting and restore it
local prev = tsuki.get_bool("show_teammate_esp", false)
tsuki.set_bool("show_teammate_esp", false)
-- ... do something ...
tsuki.set_bool("show_teammate_esp", prev)

Colors are stored as [r,g,b,a] arrays (each 0-255); get_color/set_color use a matching {r,g,b,a} table:

lua
-- Tint the enemy box fill red while a condition holds, restore otherwise
local KEY  = "enemy_box_fill_vis_color"
local base = tsuki.get_color(KEY, {r=255, g=255, b=255, a=40})  -- cache the real color ONCE

if condition then
    tsuki.set_color(KEY, {r=255, g=0, b=0, a=90})
else
    tsuki.set_color(KEY, base)                                  -- restore
end

set_color writes the LIVE setting

set_color changes the user's actual saved value and it persists to their config. For a temporary or per-modifier tint, cache the original with get_color and restore it when the condition clears - otherwise the tint can be saved permanently if the game closes while it is active.

Setting keys match the IDs TSUKI uses internally. For new script-defined settings, prefer the UI namespace - it creates a labeled widget and persists its value automatically.

The API registry

Every registered name, in a table your script can read.

FunctionReturnsDescription
tsuki.api()tableAn array of {name, kind, host, needs_internal, since}, one entry per registered name
tsuki.api_info(name)table or nilOne entry, or nil when nothing matches exactly
tsuki.reader_diag()table or nil{tick_avg_ms, tick_max_ms, ticks} for TSUKI's own loop. nil before the first measurement

The fields on an entry:

FieldNotes
nameNamespaced functions are ns.fn, player methods are p:method, callbacks and globals are bare
kindOne of function, method, global, callback or table
hostexternal or internal
needs_internalWhether your script_info has to declare the flag
sinceThe build the name arrived in

The table is built from the live registrations, so tsuki.api_info is the reliable way to check whether a call exists on this build.

The name must match exactly, prefix included: "p:get_stats", not "get_stats". tsuki.api() builds a fresh table each call; do not call it every tick.

lua
local i = tsuki.api_info("p:get_stats")
if i then
    print(i.name, i.host, i.needs_internal)   -- p:get_stats  internal  true
end

local n_internal = 0
for _, e in ipairs(tsuki.api()) do
    if e.needs_internal then n_internal = n_internal + 1 end
end
print(n_internal .. " calls need needs_internal = true")

The Lua helpers further down this page (sleep, debounce, press_ability, snap_to_target and the rest) and print are not in the table. They still work.

tsuki.reader_diag() reports TSUKI's own tick timing. A script that pushes the average up is doing too much per tick.

For what the host and needs_internal tags mean, see the Internal API Reference.

Cross-Script Modules

Scripts can share code by using export and import. A library script calls export at the top level to register a table of functions under a name. Consumer scripts call import from on_tick to fetch it.

FunctionReturnsDescription
export(name, module_table)-Register a table under name so other scripts can import it. Call at top level
import(name)table | nilFetch a module exported by another loaded script. Returns nil if the library is not loaded

WARNING

The sandbox removes eight base-library globals: require, dofile, loadfile, rawget, rawset, rawlen, rawequal, and collectgarbage. Calling any of them fails with attempt to call a nil value. export/import is the only supported mechanism for sharing code between scripts.

The sandbox loads six standard libraries: _G (base), math, string, table, coroutine, utf8. There is no os, io, or debug. Some ported library code reaches for rawget/rawset for metatable-safe access, or for collectgarbage("count") to measure memory. Edit those calls out before it will run here.

lua
-- mylib.lua - the library script
name = "mylib"
description = "Shared utilities"

export("mylib", {
    clamp = function(v, lo, hi) return math.max(lo, math.min(hi, v)) end,
    sign  = function(v) return v >= 0 and 1 or -1 end,
})
lua
-- consumer.lua
name = "consumer"
description = "Uses mylib"

local lib = nil

function on_tick()
    if not lib then lib = import("mylib") end
    if not lib then return end  -- library not loaded yet

    local clamped = lib.clamp(some_value, 0, 100)
end

Storage

Persistent key-value storage scoped per script. Values survive script reloads, game restarts, and PC restarts. Each script gets its own isolated storage at scripts\storage\<script_name>.json; keys from script A are invisible to script B.

FunctionReturnsDescription
storage.set(key, value)-Save a value under a key. Passing nil as the value deletes the key.
storage.get(key)any | nilLoad a saved value, or nil if not set
storage.keys()tableList of saved keys for this script

Values can be strings, numbers or booleans. nil deletes the key. Any other type, or a missing value (storage.set("k")), raises storage.set: unsupported value type. Tables are not supported; store fields individually or encode them as a string.

Numeric strings do not round-trip

The type check asks "is this a number?" before "is this a string?". In Lua that is true for any string convertible to a number. storage.set("k", "42") stores the number 42, and storage.get("k") returns 42, not "42" (same for "3.14", "1e3", "0x1F"). If a value must stay a string, give it a non-numeric prefix.

Writes are cached in memory and flushed to disk every ~5 seconds while the engine is ticking. One more flush runs when the Lua VM shuts down (app exit or a script reload). There is no immediate flush on each storage.set() call, and disabling a script does not flush.

Don't write storage from on_unload

The shutdown flush runs before on_unload is called, and the cache is discarded right after. Anything on_unload saves never reaches disk. While it sits in the cache it is filed under whichever script ticked last, not yours. Persist state as it changes instead.

lua
-- Track cumulative kills across game sessions
function on_kill(player)
    if not player:is_enemy() then return end
    local total = (storage.get("total_kills") or 0) + 1
    storage.set("total_kills", total)
    print("Total kills: " .. total)
end

-- Store structured data as individual keys (no json global exists)
storage.set("loadout_slot1", "burst_fire")
storage.set("loadout_slot2", "trooper_bounty")
local slot1 = storage.get("loadout_slot1")

-- Delete a key
storage.set("old_data", nil)

Helpers

Coroutine-friendly helpers available in every script covering common patterns: sleeping, debouncing, key management, and aim control. All timing helpers (sleep, wait_*) yield the coroutine and are only usable inside coroutine callbacks (e.g. on_tick).

Timing

FunctionReturnsDescription
sleep(ms)-Yield for approximately ms milliseconds
debounce(key, cooldown_ms)booleanReturn true at most once per cooldown_ms window for a given key
lua
function on_tick()
    if not debounce("my_feature", 500) then return end
    -- runs at most twice per second
end

Key and Ability Shortcuts

FunctionReturnsDescription
press_ability(slot)-Press (down + up) the key bound to the given ability slot
hold_key(key, duration?)-Hold a VK key for duration ms then release. Default 500 ms
hold_ability(slot, duration?)-Hold the key bound to an ability slot. Default 500 ms
lua
press_ability(slot.ability1)     -- tap ability 1
hold_key(VK.SPACE, 200)          -- hold Space for 200 ms
hold_ability(slot.ability3, 300) -- hold ability 3 for 300 ms

Aim Helpers

FunctionReturnsDescription
snap_to_target(target, opts?)booleanFlick aim to target using input.snap_to. Returns false if target is invalid or dead
aim_at_target(target, opts?)booleanSmooth aim at target until within threshold FOV or timeout. Returns true on success
lock_aim(target, opts?)-Start continuously aiming at target every tick (processed after on_tick)
unlock_aim()-Cancel any active aim lock
fire_and_hold(target, slot, opts?)-Press ability then continuously aim at target for hold_time ms

opts table for aim helpers:

KeyTypeDefaultNotes
bonestring | integerspine_1 (index 4) for snap_to_target/aim_at_target; bone.chest for fire_and_holdBone to aim at
projectile_velocitynumber0Projectile speed for lead compensation
hold_timenumber200fire_and_hold only - ms to keep aiming after pressing
speednumber0.5aim_at_target and lock_aim - smoothing speed handed to input.move_mouse
thresholdnumber0.5aim_at_target only - success FOV in degrees
timeoutnumber2000aim_at_target only - max ms before returning false

lock_aim reads the same speed key with the same 0.5 default. Soften a continuous lock directly; you do not need to hand-roll aim_at_target in a loop:

lua
lock_aim(target, { bone = bone.head, speed = 0.2 })   -- slow, sticky tracking

snap_to_target and fire_and_hold ignore speed; they flick through input.snap_to, which does its own smoothing.

lua
-- Flick and fire
if snap_to_target(target, { bone = bone.head }) then
    press_ability(slot.ability1)
end

-- Smooth aim with timeout
if aim_at_target(target, { speed = 0.3, threshold = 1.5, timeout = 500 }) then
    left_click()
end

-- Fire then hold aim on the target
fire_and_hold(target, slot.ability2, { hold_time = 300, bone = bone.chest })

-- Continuous tracking per tick
lock_aim(target, { bone = bone.chest })
-- later, when done:
unlock_aim()

Wait Helpers

All wait_* helpers yield the coroutine until a condition is met, then return true. They return false if the timeout elapses first (default 3000 ms).

FunctionReturnsDescription
wait_for_cast_delay(slot, timeout?)booleanYield until the ability's cast delay finishes
wait_for_cooling_down(slot, timeout?)booleanYield until the ability has STARTED cooling down (is_cooling_down becomes true). Returns true at that point, not when the cooldown finishes
wait_until_in_range(target, distance, timeout?)booleanYield until target is within distance metres
lua
function on_tick()
    press_ability(slot.ability1)
    if not wait_for_cast_delay(slot.ability1, 1000) then return end  -- wait up to 1s
    -- cast delay finished, ability is active
    if not wait_for_cooling_down(slot.ability1, 5000) then return end -- wait for CD
    -- ready again
end

Targeting Helper

FunctionReturnsDescription
find_closest_by_predicate(predicate, max_dist)player | nilFind the closest enemy within max_dist metres that satisfies predicate(player) == true
lua
-- Find the closest enemy that has a specific item
local target = find_closest_by_predicate(function(p)
    return p:has_item("phantom_strike") and p:is_alive()
end, 30.0)