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
- 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. ui.new_*returns a handle. Read the control's value withui.getand change it withui.set.- Drawing functions only work inside
paintandpaint_ui. Calling one anywhere else raises an error. - 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)
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.
Creates a checkbox. The first three arguments are the same for every new_* function.
| Parameter | Type | Description |
|---|---|---|
| tab | string | "LUA" for the script's own page, or a built-in tab such as "misc" or "visuals" |
| group | string | Column, "A" or "B" |
| name | string | Label and save key. Must be unique within the script. |
| default | boolean | Optional. 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)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 180A 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"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)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)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)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.")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)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)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)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.")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)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.
| Key | Colour |
|---|---|
| accent | The accent the user picked |
| window | Window background |
| sidebar | Sidebar background |
| panel | Panel background, a little lighter than the window |
| text | Main text |
| muted | Labels and secondary text |
| border | Dividers and outlines |
| field | Input background |
| hover | Hovered item background |
| selected | Selected 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
endclient
Event callbacks, screen and connection info, the camera, bullet traces and logging.
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)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)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)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() * 1000Game 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)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 pressedThe 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.
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.
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.
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)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))
endentity
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.
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 endIndices 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
endIndices 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())
endThe 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.49Reads 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 0The 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)
endThe 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.
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)
endThe 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) -- headis_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.
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
endget_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
endrenderer
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.
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)A 1px line between two points on the screen.
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)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)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.
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")
endDraws 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)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)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.
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.
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.
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
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.
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
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)
endEvents
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
| Event | When it runs and what it passes |
|---|---|
| paint | Every frame, before the menu is drawn. Drawing works here. |
| paint_ui | Every 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_command | Once 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_command | After the tick is processed. Passes the finished command. Changes are ignored. |
| draw_model | Before 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_stage | After each frame stage. Passes the stage number. |
| net_update_start | Before a network update is applied. |
| net_update_end | After entities are updated. Read lag records and set glow here. |
| pre_render | When the frame starts rendering. Drawing doesn't work here. |
| post_render | When the frame finishes rendering. Drawing doesn't work here. |
| override_view | Passes the camera, with fov, viewmodel_fov, position and angles. Changes are applied. |
| level_init | A map has loaded. Passes the map name. Reset per-map state here. Stored entity refs stop resolving. |
| level_shutdown | The map is about to unload. Save anything you need to keep. |
| pre_config_load | Before a config is loaded. post_config_load runs after it. |
| pre_config_save | Before a config is saved. post_config_save runs after it. |
| shutdown | The script is about to unload. Your last chance to save data and free images. |
Game events
| Event | Fields |
|---|---|
| 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_death | userid victim, attacker killer, headshot, weapon |
| weapon_fire | userid shooter, weapon |
| bullet_impact | userid shooter, x, y, z where the bullet hit |
| round_start | A new round has started. |
| round_end | winner team, reason |
| bomb_planted | userid planter, site |
| bomb_begindefuse | userid defuser, haskit |
| bomb_abortdefuse | userid defuser |
| bomb_defused | userid defuser, site |
| bomb_exploded | userid planter, site |
User IDs in game events are different from entity indices. Pass them through client.userid_to_entindex before using them with entity.*.