Nothing matches that search.

Lua API

Scripts are Lua 5.4 files in Documents\jamalhook\scripts. Open the Scripts tab in the menu, press Refresh, pick a file and press Load. Each script gets its own Lua state, so one script can't see another's globals.

The example pack includes a shared module with helpers for panels, menu colours and entity state. Load it with require("poc.common").

Four rules worth knowing first

  1. Top-level code runs once, when the script loads. Anything that should happen every frame or every tick goes in a callback registered with client.set_event_callback.
  2. ui.new_* returns a handle. Read the control's value with ui.get and change it with ui.set.
  3. Drawing functions only work inside paint and paint_ui. Calling one anywhere else raises an error.
  4. A control's name is also the key its value is saved under. Rename the control and the saved value is lost.
local enabled = ui.new_checkbox("LUA", "A", "Box ESP", true)
local colour  = ui.new_color_picker("LUA", "A", "Box colour", 255, 255, 255, 255)

client.set_event_callback("paint", function()
    if not ui.get(enabled) then return end
    local r, g, b, a = ui.get(colour)

    for _, ent in ipairs(entity.get_players(true)) do
        local x1, y1, x2, y2, alpha = entity.get_bounding_box(ent)
        if x1 then
            renderer.rectangle_outline(x1, y1, x2 - x1, y2 - y1,
                r, g, b, math.floor(a * alpha))
        end
    end
end)
The Scripts tab, with the script editor on the left and the script actions on the right
The Scripts tab. Create, edit, load and reload scripts from inside the game without restarting it. The editor shows the same file you'd open in a text editor.
A script's own settings page with checkboxes, dropdowns and colour pickers created from Lua
Controls made with ui.new_*. A script that uses the "LUA" tab gets its own page in the sidebar. Column "A" and "B" are the two panels, and each control's name is its label.

ui

Creates and reads menu controls. Each new_* call returns a handle that you pass to ui.get and ui.set. Script control values are saved per script in scripts/data/<script>/ui.json, separately from the main config, and come back when the script is reloaded.

ui.new_checkbox(tab, group, name, default)→ handle

Creates a checkbox. The first three arguments are the same for every new_* function.

ParameterTypeDescription
tabstring"LUA" for the script's own page, or a built-in tab such as "misc" or "visuals"
groupstringColumn, "A" or "B"
namestringLabel and save key. Must be unique within the script.
defaultbooleanOptional. Used when nothing is saved yet.
local enabled = ui.new_checkbox("LUA", "A", "ESP: enabled", true)

client.set_event_callback("paint", function()
    if not ui.get(enabled) then return end
    -- only runs while the box is ticked
end)
ui.new_slider(tab, group, name, min, max, default)→ handle

Creates a slider. min must be less than max, and default is optional. ui.get returns a number in that range.

local fov = ui.new_slider("LUA", "A", "Aimbot: FOV", 1, 180, 10)
local v = ui.get(fov)  -- 1 to 180
ui.new_combobox(tab, group, name, ...options)→ handle

A dropdown with one choice. Pass the options as separate strings or as one table. ui.get returns the selected string.

local style = ui.new_combobox("LUA", "A", "Chams: material",
    "Lit", "Flat", "Rim", "Wireframe")

-- ui.get(style) is "Lit", "Flat", "Rim" or "Wireframe"
ui.new_multiselect(tab, group, name, ...options)→ handle

A dropdown where several options can be ticked. ui.get returns a table of the selected strings, which is empty when nothing is selected.

local parts = ui.new_multiselect("LUA", "A", "ESP: draw",
    "Box", "Name", "Health", "Weapon", "Distance")

client.set_event_callback("paint", function()
    local list = ui.get(parts)
    local function wants(s)
        for _, v in ipairs(list) do if v == s then return true end end
    end
    if wants("Box") then
        -- draw the box
    end
end)
ui.new_color_picker(tab, group, name, r, g, b, a)→ handle

An RGBA colour picker. The defaults are integers from 0 to 255 and each one falls back to 255 if left out. ui.get returns four values: r, g, b, a.

local col = ui.new_color_picker("LUA", "A", "ESP: colour", 255, 255, 255, 255)

client.set_event_callback("paint", function()
    local r, g, b, a = ui.get(col)
    renderer.rectangle_outline(x1, y1, w, h, r, g, b, a)
end)
ui.new_button(tab, group, name, callback)→ handle

A button. callback runs during the next menu update after the click and receives the button's handle. You can call any other ui.* or database.* function from it.

ui.new_button("LUA", "B", "Stats: clear history", function()
    saved_rounds = {}
    database.write("round_history", saved_rounds)
end)
ui.new_label(tab, group, text)→ handle

A line of text in the menu, for notes and warnings next to your controls. ui.get returns the text.

ui.new_label("LUA", "B", "Colours apply on the next round.")
ui.new_string(tab, group, name, default)→ handle

A saved string with nothing drawn in the menu. Use it to keep text between sessions, such as where the user dragged a panel.

local pos = ui.new_string("LUA", "A", "Panel: saved position", "")

-- save after a drag
ui.set(pos, string.format("%.8f,%.8f", rx, ry))
ui.save()

-- restore on load
local sx, sy = ui.get(pos):match("^([%d%.]+),([%d%.]+)$")
rx, ry = tonumber(sx), tonumber(sy)
ui.get(handle)→ value
ui.set(handle, value)

Reads or changes a control's value. The type depends on the control: boolean for checkboxes, number for sliders, string for comboboxes and hidden strings, a table of strings for multiselects, and four numbers for colour pickers. To set a colour picker, pass four numbers. To set a multiselect, pass a table of strings. Combobox values have to be one of its options.

local cb  = ui.new_checkbox("LUA", "A", "x", true)
local sl  = ui.new_slider("LUA", "A", "y", 0, 100, 50)
local co  = ui.new_combobox("LUA", "A", "z", "A", "B")
local clr = ui.new_color_picker("LUA", "A", "w", 255, 0, 0, 255)

ui.get(cb)            -- true or false
ui.get(sl)            -- 0 to 100
ui.get(co)            -- "A" or "B"
local r, g, b, a = ui.get(clr)

ui.set(cb, false)
ui.set(sl, 75)
ui.set(clr, 0, 255, 0, 255)
ui.set_callback(handle, fn)

Runs fn after the control's value changes, during the next menu update. fn receives the handle. Each control has one callback, so calling this again replaces the old one.

local glow_on = ui.new_checkbox("LUA", "B", "Chams: glow", false)

ui.set_callback(glow_on, function(handle)
    if not ui.get(handle) then
        clear_all_glows()
    end
end)
ui.set_tooltip(handle, text)

Adds text that shows when the user hovers the control. One sentence about what the label leaves out is usually enough.

ui.set_tooltip(keep, "Draws the normal model under the chams.")
ui.set_visible(handle, visible)

Shows or hides a control. A hidden control keeps its value, still saves, and can still be read and set from code. This lets you replace an old control while keeping its saved value around to migrate from.

-- the X/Y sliders were replaced by dragging; hide them
ui.set_visible(old_x_slider, false)
ui.set_visible(old_y_slider, false)
ui.is_menu_open()→ boolean
ui.mouse_position()→ x, y
ui.menu_position()→ x, y
ui.menu_size()→ w, h
ui.save()

is_menu_open is true while the menu is showing. The next three return the cursor position, the menu's top-left corner and the menu's size, all in screen pixels. Together they tell a draggable panel whether the cursor is over the menu. save writes the script's control values to disk right away; they are also saved when the script unloads.

client.set_event_callback("paint_ui", function()
    if not ui.is_menu_open() then return end
    local mx, my = ui.mouse_position()
    local nx, ny = ui.menu_position()
    local nw, nh = ui.menu_size()
    local over_menu = mx >= nx and mx < nx + nw and my >= ny and my < ny + nh
    -- start a panel drag only when over_menu is false
end)
ui.theme()→ table

Returns the menu's current colours, each as an {r, g, b} table. Panels drawn with these follow the user's theme. Cache it and refresh it every so often. poc.common already does this.

KeyColour
accentThe accent the user picked
windowWindow background
sidebarSidebar background
panelPanel background, a little lighter than the window
textMain text
mutedLabels and secondary text
borderDividers and outlines
fieldInput background
hoverHovered item background
selectedSelected item background
local theme, next_read = nil, 0

local function accent()
    local now = client.timestamp()
    if now >= next_read then
        theme, next_read = ui.theme(), now + 500
    end
    local a = theme.accent
    return a.r, a.g, a.b
end

client

Event callbacks, screen and connection info, the camera, bullet traces and logging.

client.set_event_callback(event, fn)

Runs fn every time event fires. Callbacks run in script load order, then in the order they were registered. Registering the same function twice does nothing. client.unset_event_callback(event, fn) removes it again. Game events such as player_hurt pass a table of fields. See Events for what each engine event passes.

If a callback raises an error, the error and a traceback go to the console and that callback is turned off. Your other callbacks keep running.

client.set_event_callback("level_init", function(map)
    -- runs once per map load
    my_entries = {}
end)

client.set_event_callback("player_hurt", function(e)
    -- e.attacker, e.userid, e.dmg_health, e.hitgroup
    if e.hitgroup == 1 then
        -- headshot
    end
end)
client.screen_size()→ w, h

Returns the screen width and height in pixels. Call it inside paint each frame instead of storing it at load, because the user can resize the game window.

client.set_event_callback("paint", function()
    local sw, sh = client.screen_size()
    renderer.text(sw / 2, 20, 255, 255, 255, 255, "c", 0, "centered")
end)
client.timestamp()→ number

A millisecond clock that only moves forward. The starting point is arbitrary, so only the difference between two readings means anything. It keeps counting while the game is paused or disconnected, which makes it a good fit for throttling, animations and HUD timers. For game time use globals.curtime(). For the date use client.unix_time().

local last = 0

client.set_event_callback("paint", function()
    local now = client.timestamp()
    if now - last >= 100 then
        last = now
        -- runs about 10 times a second
    end
end)
client.latency()→ number
client.is_connected()→ boolean

latency is incoming plus outgoing latency in seconds. Multiply by 1000 for milliseconds. is_connected is true while the client is on a server.

if not client.is_connected() then return end
local ping_ms = client.latency() * 1000
client.userid_to_entindex(userid)→ integer

Game events identify players by user ID, in fields like e.userid and e.attacker. The entity.* functions take entity indices. Convert with this first. It returns 0 if the user ID is unknown.

client.set_event_callback("player_hurt", function(e)
    local attacker = client.userid_to_entindex(e.attacker)
    local victim   = client.userid_to_entindex(e.userid)
    local name = entity.get_player_name(victim) or "?"
end)
client.key_state(vk)→ boolean
client.key_pressed(vk)→ boolean

key_state is true for as long as the key is held. key_pressed is true when the key has just gone down. vk is a Windows virtual-key code, for example 0x01 for the left mouse button and 0x02 for the right.

local holding = client.key_state(0x01)    -- left mouse held
local tapped  = client.key_pressed(0x46)  -- F just pressed
client.camera_angles()→ pitch, yaw

The current view angles in degrees. Combine with client.eye_position() to get the line the player is looking along, for example to find the enemy closest to the crosshair.

client.eye_position()→ x, y, z | nil

The local player's eye position in world units, or nil when there's no local player. This is the usual start point for client.trace_bullet.

client.trace_bullet(shooter, x1, y1, z1, x2, y2, z2)→ number | nil

Simulates a bullet from the first point to the second using the aimbot's own penetration code and the shooter's weapon. Returns the damage the bullet would still have on arrival, or 0 if it doesn't get there. Returns nothing outside a game, when the shooter is dead, or when the local player has no weapon. Use client.scale_damage(player, damage, hitgroup) to apply armour and hitgroup scaling to the result.

Each call is a full penetration trace. Run it from setup_command every few ticks, not from paint.
local tick = 0

client.set_event_callback("setup_command", function()
    tick = tick + 1
    if tick % 3 ~= 0 then return end

    local me = entity.get_local_player()
    local ex, ey, ez = client.eye_position()
    if not me or not ex then return end

    for _, p in ipairs(entity.get_players(true)) do
        local hx, hy, hz = entity.hitbox_position(p, 0)  -- head
        if hx then
            local dmg = client.trace_bullet(me, ex, ey, ez, hx, hy, hz) or 0
            local real = client.scale_damage(p, dmg, 1)       -- hitgroup 1 = head
        end
    end
end)
client.error_log(message)

Writes a message to the error log. Use it for real failures such as bad image data or missing values. For everyday debug output, print writes to the game console with your script's name in front.

local img, err = renderer.load_image(raw)
if not img then
    client.error_log("my_script: image load failed: " .. tostring(err))
end

entity

Reads players, weapons and other game entities. Entities are passed around as integer indices, and the game reuses an index once its entity is gone. If you need to keep hold of an entity across events, store an entity.ref instead.

entity.get_local_player()→ integer | nil

Your own player's index, or nil when you aren't in a game. You may still be dead, so check entity.is_alive as well.

local me = entity.get_local_player()
if not me or not entity.is_alive(me) then return end
entity.get_players(enemies_only)→ table

Indices of every living player, dormant ones included. Pass true to get enemies only. Without it the list includes teammates and you.

local me = entity.get_local_player()
for _, p in ipairs(entity.get_players()) do
    if p ~= me and not entity.is_dormant(p) then
        -- every other player the server is updating
    end
end
entity.get_all(classname)→ table

Indices of every entity with the given network class name, for example "CPlantedC4" for a planted bomb.

for _, bomb in ipairs(entity.get_all("CPlantedC4")) do
    local blow = entity.get_prop(bomb, "m_flC4Blow")
    local remaining = math.max(0, blow - globals.curtime())
end
entity.get_origin(entindex)→ x, y, z | nil

The entity's position in world units. For a player this is at their feet. About 52.5 units make a metre.

local px, py, pz = entity.get_origin(player)
local mx, my, mz = entity.get_origin(entity.get_local_player())
local metres = math.sqrt((px - mx)^2 + (py - my)^2 + (pz - mz)^2) / 52.49
entity.get_prop(entindex, name [, index])→ value
entity.set_prop(entindex, name, value [, index])

Reads or writes a networked property by name, such as m_iHealth. Vector properties come back as three numbers. Array indices start at 0. Integer properties the SDK doesn't know the size of are refused, so wrap reads of unusual props in pcall. Strings are read-only, and the game can overwrite anything you write on its next network update.

local function prop(ent, name)
    local ok, a, b, c = pcall(entity.get_prop, ent, name)
    if ok then return a, b, c end
end

local hp    = prop(player, "m_iHealth")
local flags = prop(player, "m_fFlags")
local on_ground = flags and (flags & 1) ~= 0

local vx, vy = prop(player, "m_vecVelocity")
local speed = vx and math.sqrt(vx * vx + vy * vy) or 0
entity.get_bounding_box(entindex)→ x1, y1, x2, y2, alpha

The entity's box on screen, in pixels. alpha runs from 0 to 1 and drops toward 0 as a player goes dormant, so multiply your opacity by it to fade them out smoothly. Returns nil when the entity is off screen.

local x1, y1, x2, y2, alpha = entity.get_bounding_box(player)
if x1 and alpha > 0 then
    local a = math.floor(255 * math.min(1, alpha))
    renderer.rectangle_outline(x1, y1, x2 - x1, y2 - y1, 255, 255, 255, a)
end
entity.get_player_name(entindex)→ string | nil

The player's name as UTF-8, or nil for anything that isn't a player. Names can contain any character and can change during a match. entity.get_steam64 returns the Steam ID as a decimal string.

entity.get_player_weapon(entindex)→ integer | nil
entity.get_weapon_info(weapon)→ table | nil

get_player_weapon returns the index of the weapon the player is holding. Pass that to get_weapon_info for a table with name, id, type, clip, max_clip, damage, range, armor_ratio, penetration, max_speed and the spread and inaccuracy values.

local weapon = entity.get_player_weapon(player)
local info   = weapon and entity.get_weapon_info(weapon)
if info then
    local name = (info.name or ""):gsub("^weapon_", ""):upper()
    local ammo = string.format("%d/%d", info.clip or 0, info.max_clip or 0)
end
entity.hitbox_position(entindex, hitbox_id)→ x, y, z | nil

The centre of a hitbox in world units, from the player's current bones. Common IDs: 0 head, 3 pelvis, 4 stomach, 6 chest, 12 right foot. Returns nil if the player can't be read.

local hx, hy, hz = entity.hitbox_position(player, 0)  -- head
entity.is_alive(entindex)→ boolean
entity.is_enemy(entindex)→ boolean
entity.is_dormant(entindex)→ boolean

is_alive means health above 0. is_enemy means the other team. is_dormant means the server has stopped sending updates for the entity, usually because it's far away or out of sight, so its position is out of date. All three return false for an invalid index.

entity.ref(entindex)→ ref
entity.resolve(ref)→ integer | nil

An index can point at a different entity later on. ref returns a handle tied to this specific entity. resolve turns it back into the current index, or nil if the entity is gone or the map has changed. Every entity.* function also accepts a ref directly.

local r = entity.ref(some_player)

-- later, in another callback
local p = entity.resolve(r)
if p and entity.is_alive(p) then
    -- same player as before
end
entity.get_animstate(player)→ table | nil
entity.get_anim_layers(player)→ table
entity.get_poses(player)→ table
entity.get_records(player [, n])→ table

get_animstate returns a copy of the player's animation state, with fields including eye_yaw, abs_yaw, move_yaw, speed, max_desync, on_ground, landing, on_ladder, duration_moving and duration_still.

get_anim_layers returns the 13 animation layers (sequence, activity, weight, cycle, playback_rate and more). get_poses returns the 24 pose parameters. get_records returns up to 32 of the aimbot's lag records for the player, newest first, each with fields such as lag, yaw, lby, away and fake_walk. Everything is a copy, so changing it has no effect on the game.

local s = entity.get_animstate(player)
if s then
    local desync = s.eye_yaw - s.abs_yaw
end

local records = entity.get_records(player, 12)
local latest = records and records[1]  -- newest
if latest then
    -- latest.lag, latest.yaw, latest.lby, latest.fake_walk
end

renderer

Draws on the screen. Drawing calls only work inside paint and paint_ui and raise an error anywhere else. Fonts, text measuring and world_to_screen work everywhere. (0, 0) is the top-left corner, and colours are r, g, b, a from 0 to 255.

renderer.rectangle(x, y, w, h, r, g, b, a)
renderer.rectangle_outline(x, y, w, h, r, g, b, a)

A filled rectangle, or a 1px outline. renderer.gradient, renderer.triangle, renderer.circle and renderer.circle_outline cover other shapes.

renderer.rectangle(10, 10, 100, 60, 20, 20, 28, 200)
renderer.rectangle_outline(10, 10, 100, 60, 255, 255, 255, 160)
renderer.line(x1, y1, x2, y2, r, g, b, a)

A 1px line between two points on the screen.

renderer.text(x, y, r, g, b, a, flags, max_width, ...)

Draws text in the menu font. Any arguments after max_width are joined into one string. flags is a string of letters, or nil for normal text: c centres on x, r right-aligns, - uses the small font, b the bold font and + the large indicator font. A max_width of 0 means no limit.

A colour code inside the text, written as \a followed by RRGGBBAA, changes the colour of everything after it. renderer.measure_text(flags, ...) returns the width and height the text would take up.

renderer.text(x, y, 255, 255, 255, 255, nil, 0, "left-aligned")
renderer.text(x, y, 255, 255, 255, 255, "c", 0, "centred on x")
renderer.text(x, y, 255, 255, 255, 255, "r", 0, "ends at x")
renderer.text(x, y, 255, 255, 255, 255, "c-", 0, "small and centred")
renderer.text(x, y, 255, 255, 255, 255, "b", 0, "HP \a5CBC9BFF", hp)
renderer.create_font(name, size, weight, flags)→ font

Makes a font for renderer.text_font. weight works like CSS, with 400 for regular and 700 for bold. flags are Source font flags, where 16 turns on anti-aliasing. Create fonts once, at load. They are cached and shared, and released when the script unloads.

local font = renderer.create_font("Segoe UI", 14, 400, 16)

client.set_event_callback("paint", function()
    renderer.text_font(font, 20, 20, 255, 255, 255, 255, "hello", 0)
end)
renderer.text_font(font, x, y, r, g, b, a, text, align)
renderer.measure_font(font, text)→ w, h

Draws text with a font from renderer.create_font. align is 0 for left, 1 for right and 2 for centre. measure_font returns how much space the text needs.

renderer.world_to_screen(x, y, z)→ sx, sy | nil

Converts a point in the world to screen pixels. Returns nil when the point is behind the camera.

local ox, oy, oz = entity.get_origin(player)
local sx, sy = renderer.world_to_screen(ox, oy, oz + 80)
if sx then
    renderer.text(sx, sy, 255, 255, 255, 255, "c", 0, "label")
end
renderer.indicator(r, g, b, a, ...)

Draws a line of large text on the left side of the screen, in the same style as the built-in indicators. Each call adds a line to the stack, so you don't pick the position. Good for short status text that has to be readable at a glance.

renderer.indicator(255, 190, 80, 255, "WALL ", 68)
renderer.load_image(bytes)→ image | nil, err

Loads a PNG, JPEG, BMP or GIF from a string of bytes. On failure it returns nil and an error message. Read the bytes from a file with file.read, or embed them in the script with base64.decode. A script can hold 64 images of up to 2048 pixels per side. Free them with renderer.free_image when you're done.

local PNG = "iVBORw0KGgo..."  -- the PNG as Base64

local image
local raw = base64.decode(PNG)
if raw then
    local h, err = renderer.load_image(raw)
    if h then
        image = h
    else
        client.error_log("load_image: " .. tostring(err))
    end
end

client.set_event_callback("paint", function()
    if not image then return end
    local sw, sh = client.screen_size()
    renderer.image(image, sw / 2 - 16, sh / 2 - 16, 32, 32, 255, 255, 255, 200)
end)

client.set_event_callback("shutdown", function()
    if image then renderer.free_image(image) end
end)
renderer.image(image, x, y, w, h [, r, g, b, a])
renderer.image_rotated(image, cx, cy, w, h, degrees [, r, g, b, a])

Draws a loaded image stretched to w by h, tinted by the colour if you pass one. image_rotated draws it around the centre point (cx, cy), rotated by degrees. renderer.image_size(image) returns the original width and height.

globals

Timing from the game engine. These follow game time, so they stop when the game does.

globals.curtime()→ number
globals.absoluteframetime()→ number
globals.tickinterval()→ number
globals.chokedcommands()→ integer

curtime is the server's game time in seconds, the same clock used by timers like m_flC4Blow. absoluteframetime is how long the last frame took, in seconds. Scale animations by it so they run at the same speed at any frame rate. tickinterval is the length of one tick in seconds, so 1 / tickinterval() is the tick rate. chokedcommands is the number of commands waiting to be sent.

Also available: realtime, frametime, framecount, tickcount, servertickcount, maxplayers and mapname.

-- move cur toward target at the same speed at any frame rate
local function smooth(cur, target, speed)
    local dt = globals.absoluteframetime()
    return cur + (target - cur) * math.min(1, dt * speed)
end

-- seconds until the bomb goes off
local blow = entity.get_prop(bomb, "m_flC4Blow")
local remaining = math.max(0, blow - globals.curtime())

materials

Custom materials for chams. Create them once when the script loads, then draw with them from a draw_model callback, which is the only place materials.draw works.

materials.create(name, vmt)→ material
materials.find(path)→ material
materials.draw(material, r, g, b, a [, ignore_z])

create builds a material from VMT text. UnlitGeneric gives flat colour, and VertexLitGeneric gives lighting and reflections. find loads one of the game's own materials. A script can have up to 32.

draw draws the current model once more with the material. Pass true for ignore_z to draw it through walls. Set e.hide = true to skip the normal model. The built-in chams still draw afterwards unless the user has them off.

local flat = materials.create("my_flat", [[
    "UnlitGeneric" {
        "$basetexture" "vgui/white_additive"
        "$model" "1"  "$nofog" "1"
    }
]])

client.set_event_callback("draw_model", function(e)
    if e.kind ~= "player" or not e.enemy then return end
    materials.draw(flat, 255, 60, 60, 120, true)   -- behind walls
    materials.draw(flat, 255, 60, 60, 255, false)  -- in view
    e.hide = true
end)

glow

The game's outline glow. A glow you set stays on, and overrides the built-in glow settings, until you clear it, the script unloads or the map changes.

glow.set(entindex, r, g, b, a [, options])
glow.clear(entindex)

options is a table with occluded (default true), unoccluded (default false) and full_bloom (default false). occluded shows the glow behind walls and unoccluded shows it in plain view. clear removes the glow, and is safe to call on an entity that has none.

local glowing = {}

client.set_event_callback("net_update_end", function()
    for _, p in ipairs(entity.get_players(true)) do
        if not glowing[p] then
            glow.set(p, 255, 60, 100, 160, { occluded = true })
            glowing[p] = true
        end
    end
end)

client.set_event_callback("level_init", function()
    glowing = {}  -- glows are cleared on map change
end)

sound

sound.play(path)

Plays a sound from the game's sound/ folder, for example "buttons/arena_switch_press_02.wav". It plays at full volume and doesn't come from any position in the world. For hit sounds, play at most one per event.

client.set_event_callback("player_hurt", function(e)
    if client.userid_to_entindex(e.attacker) == entity.get_local_player() then
        sound.play("buttons/arena_switch_press_02.wav")
    end
end)

database

Key-value storage for each script, kept in scripts/data/<script>/. It survives game restarts and config changes. Values are stored as JSON, so strings, numbers, booleans and tables all work, as long as a table doesn't contain itself.

database.read(key)→ value | nil
database.write(key, value)

read returns the stored value, or nil if there isn't one, so give it a default with or. write saves to disk straight away, and writing nil deletes the key. Every write goes to disk, so save when something changes, like at the end of a round.

local history = database.read("round_history") or { rounds = {} }

local function save_round()
    history.rounds[#history.rounds + 1] = { dealt = round.dealt, kills = round.kills }
    while #history.rounds > 12 do table.remove(history.rounds, 1) end
    database.write("round_history", history)
end

client.set_event_callback("round_end", save_round)
client.set_event_callback("shutdown", save_round)

base64

base64.decode(text)→ string | nil
base64.encode(data [, url_safe])→ string

decode turns Base64 text back into bytes and returns nil if the text isn't valid Base64. It's mostly used to ship an image inside a script: encode the PNG once, paste the text into your script, and decode it at load for renderer.load_image.

local PNG = "iVBORw0KGgoAAAANSUhEUgAA..."

local raw = base64.decode(PNG)
if raw then
    local image, err = renderer.load_image(raw)
end

Events

Names you can pass to client.set_event_callback. Engine events come from the cheat. Any other name is looked up as a Source game event, and the callback gets a table with name and the event's fields.

Engine events

EventWhen it runs and what it passes
paintEvery frame, before the menu is drawn. Drawing works here.
paint_uiEvery frame, after the menu is drawn. Drawing works here too. Use it for anything that should sit on top of the menu, like draggable panels.
setup_commandOnce per tick, while the command is built. Passes cmd. Changes to buttons, pitch, yaw and the move fields are applied. A good place for per-tick work like bullet traces.
finish_commandAfter the tick is processed. Passes the finished command. Changes are ignored.
draw_modelBefore each model is drawn. Passes kind ("player", "arms", "viewmodel", "weapon" or "other"), entity, enemy and local. Set hide to true to skip the normal draw.
frame_stageAfter each frame stage. Passes the stage number.
net_update_startBefore a network update is applied.
net_update_endAfter entities are updated. Read lag records and set glow here.
pre_renderWhen the frame starts rendering. Drawing doesn't work here.
post_renderWhen the frame finishes rendering. Drawing doesn't work here.
override_viewPasses the camera, with fov, viewmodel_fov, position and angles. Changes are applied.
level_initA map has loaded. Passes the map name. Reset per-map state here. Stored entity refs stop resolving.
level_shutdownThe map is about to unload. Save anything you need to keep.
pre_config_loadBefore a config is loaded. post_config_load runs after it.
pre_config_saveBefore a config is saved. post_config_save runs after it.
shutdownThe script is about to unload. Your last chance to save data and free images.

Game events

EventFields
player_hurt userid victim, attacker attacker, dmg_health health lost, hitgroup 1 head, 2 chest, 3 stomach, 4 and 5 arms, 6 and 7 legs
player_deathuserid victim, attacker killer, headshot, weapon
weapon_fireuserid shooter, weapon
bullet_impactuserid shooter, x, y, z where the bullet hit
round_startA new round has started.
round_endwinner team, reason
bomb_planteduserid planter, site
bomb_begindefuseuserid defuser, haskit
bomb_abortdefuseuserid defuser
bomb_defuseduserid defuser, site
bomb_explodeduserid planter, site

User IDs in game events are different from entity indices. Pass them through client.userid_to_entindex before using them with entity.*.