Scripting Reference

Nonagon Lua API

Full reference for the Nonagon scripting interface. Scripts live in C:\nonagon\scripts\ and are hot-reloaded from the UI.

v2.4CS2 / Source 2Lua 5.4Controller-based entity API
ℹ
New in v2.4: on_pre_move (every command), scripted movement (cmd.set_button, cmd.add_button_step, silent cmd.set_view_angles, movement.block_features), grenade simulation on the CS2 server model (utils.simulate_grenade, utils.simulate_throw), json, clipboard, input, more draw / entity / ui functions, ragebot and anti-aim state. New items are marked new.

callbacks

Register event handlers. A script can register one handler per event; registering again replaces it. All handlers are dropped when the script is unloaded or reloaded.

callbacks.register(event_name, function)
→boolean
Register a function for an event. Call at the top level of your script. Returns false for an unknown event name.
callbacks.unregister(event_name)new
→boolean
Remove the handler of an event.
callbacks.set_enabled(event_name, enabled)new
→boolean
Pause / resume a handler without removing it.
callbacks.is_enabled(event_name)new
→boolean
callbacks.list()new
→table
Names of the events this script has handlers for.
callbacks.clear()new
Remove every handler of this script.

Available Events

on_render
function()
Every frame — use draw.* here
on_pre_movenew
function()
Every command, before anti-aim, movement features and the ragebot — the place for cmd.* (scripted movement, buttons, view angles). Also runs while a grenade is thrown.
on_createmove
function()
Every command while anti-aim runs (anti-aim on, not throwing a grenade, not in the freeze period) — antiaim.* and cmd.* here. For anything that must run every command use on_pre_move.
on_antiaimnew
function(pitch, yaw, manual_state, freestanding, lua_override)
The angles anti-aim sent: on a state change, else every 8 ticks
on_player_hurt
function(attacker, victim, damage, health, hitgroup)
Player takes damage (names)
on_player_death
function(attacker, victim)
Player dies (names)
on_weapon_firenew
function(player, weapon)
A player fires
on_grenade_thrownnew
function(player, weapon)
A player throws a grenade
on_item_purchasenew
function(player, item)
A player buys an item
on_round_start
function()
Round begins
on_round_end
function()
Round ends
on_bullet_impact
function(idx, x, y, z)
Bullet hits a surface
on_ragebot_targetnew
function(idx, valid, damage, hitbox, hitchance, required_hc, fov, distance, x, y, z)
The ragebot picked a target
on_ragebot_shotnew
function(idx, damage, hitbox, hitchance, fov, distance, x, y, z, pitch, yaw)
The ragebot fired
on_level_init
function(map_name)
Map loads
on_level_shutdown
function()
Map unloads
callbacks.register("on_render", function()
    draw.text(10, 10, "Hello Nonagon", 255, 255, 255, 255)
end)

callbacks.register("on_player_hurt", function(attacker, victim, dmg, hp, hg)
    client.log(attacker .. " hit " .. victim .. " for " .. dmg .. " dmg")
end)

client

Core client functions — screen, time, view, player info, system info, console, notifications, deferred calls.

client.log(message)
Print a message to this script's log panel in the UI.
client.notify(text, type?)new
Show a cheat notification. type: 0 default, 1 info (default), 2 success, 3 warning, 4 error.
client.screen_size()
→number, number
Screen width and height in pixels.
client.world_to_screen(x, y, z)
→number, number, boolean
Project a world point to the screen. The third value is true when it is on screen.

Time & State

client.curtime()
→number
Game time in seconds.
client.tickcount()new
→integer
Current tick. Alias client.get_tickcount.
client.frame_time()new
→number
Last frame time in seconds. Alias client.frametime.
client.get_fps()new
→number
client.is_in_game()new
→boolean
client.is_connected()new
→boolean
client.is_in_thirdperson()
→boolean

View & Local Player

client.get_view_angles()new
→number, number, number
The camera angles (pitch, yaw, roll) — what the player looks at, not what anti-aim sends.
client.set_view_angles(pitch, yaw)new
Turn the camera. Works in any callback.
client.get_eye_pos()
→number, number, number
Local player eye position.
client.get_local_player_name()
→string

System & Info

client.get_map_name()new
→string
Short map name ("de_mirage"), "" outside a map.
client.get_map_data()
→string, string
Full and short map name.
client.get_scripts_dir()new
→string
C:\nonagon\scripts\ — for the script's own data files.
client.get_date()
→string
Local date, YYYY-MM-DD.
client.get_unixtime()
→integer
Seconds since epoch.
client.get_timestamp()
→integer
Milliseconds since epoch.
client.get_system_time()
→integer, integer, integer
Local hour, minute, second.
client.get_product_version()
→string
client.get_game_directory()
→string
client.get_username()
→string
client.get_config_name()
→string
client.get_active_scripts()
→table
Names of the loaded scripts.
client.get_mouse_wheel_delta()
→number

Console & Networking

client.console_exec(cmd)
Execute a console command.
client.net_channel()
→number, number
Outgoing and incoming latency in ms.

Deferred Execution

client.execute_after(seconds, func)
Call func after the delay (checked every frame).
client.execute_after(2.0, function()
    client.notify("Two seconds passed", 2)
end)

draw

Rendering primitives. Only inside on_render. Colors are RGBA 0–255 and default to 255.

draw.line(x1, y1, x2, y2, r, g, b, a, thickness?)
draw.rect(x, y, w, h, r, g, b, a)
Outlined rectangle, x, y = top-left corner.
draw.rect_filled(x, y, w, h, r, g, b, a)
draw.rounded_rect(x, y, w, h, rounding, r, g, b, a, thickness?)new
draw.rounded_rect_filled(x, y, w, h, rounding, r, g, b, a)new
draw.gradient_rect_filled(x, y, w, h, r1, g1, b1, a1, r2?, g2?, b2?, a2?)new
Horizontal gradient from the first color to the second.
draw.circle(x, y, radius, r, g, b, a)
draw.circle_filled(x, y, radius, r, g, b, a)
draw.arc(x, y, radius, start_deg, end_deg, r, g, b, a, thickness?)new
draw.triangle(x1, y1, x2, y2, x3, y3, r, g, b, a, thickness?)new
draw.triangle_filled(x1, y1, x2, y2, x3, y3, r, g, b, a)new
draw.circle_3d(x, y, z, radius, r, g, b, a, thickness?, segments?)new
A flat circle in the world around a point (on the ground). segments 8–128, default 48.
draw.text(x, y, text, r, g, b, a)
draw.text_size(text)new
→number, number
Width and height of the text in pixels.

input

Keyboard and mouse. Keys are Windows virtual-key codes (the same as the menu's keybinds).

input.is_key_down(vk)new
→boolean
input.is_key_pressed(vk)new
→boolean
True on the frame the key went down.
input.is_mouse_down(button?)new
→boolean
0 left (default), 1 right, 2 middle.
input.is_mouse_pressed(button?)new
→boolean
input.mouse_position()new
→number, number
Alias input.get_mouse_pos.

entity

Player functions take a controller index; the generic ones any entity index.

Players

entity.get_local_player()
→integer | nil
entity.get_players()
→table
Controller indices of alive enemies.
entity.get_all_players()
→table
Every player controller, dead and local included.
entity.is_alive(ctrl_idx)
→boolean
entity.is_enemy(ctrl_idx)
→boolean
entity.is_bot(ctrl_idx)new
→boolean
entity.get_name(ctrl_idx)
→string
entity.get_health(ctrl_idx)
→integer
entity.get_armor(ctrl_idx)
→integer
entity.get_team(ctrl_idx)
→integer
2 = T, 3 = CT.
entity.get_ping(ctrl_idx)new
→integer
entity.get_origin(ctrl_idx)
→number, number, number
entity.get_eye_pos(ctrl_idx)
→number, number, number
entity.get_velocity(ctrl_idx)
→number, number, number
entity.get_flags(ctrl_idx)
→integer
Flags bitmask (FL_ONGROUND = 1, FL_DUCKING = 2).
entity.get_duck_amount(ctrl_idx)new
→number
0 standing … 1 fully ducked.
entity.get_move_type(ctrl_idx)new
→integer
entity.get_simulation_time(ctrl_idx)new
→number
entity.get_pawn_idx(ctrl_idx)
→integer
Pawn entity index, -1 if none.

Weapons

entity.get_active_weapon(ctrl_idx)new
→integer
Entity index of the active weapon, -1 if none.
entity.get_weapon_name(ctrl_idx)new
→string
Class name of the active weapon ("weapon_smokegrenade").
entity.get_weapon_clip(ctrl_idx)new
→integer, integer, boolean
Clip 1, clip 2, reloading.
entity.get_weapon_type(ctrl_idx)new
→integer
The weapon type of its data (knife, pistol, grenade …).
entity.get_weapon_zoom(ctrl_idx)new
→integer
Zoom level of the active weapon.
entity.get_grenade_state(ctrl_idx)new
→boolean, boolean, number, number
For the active weapon: is a grenade, pin pulled, throw time (0 until released), throw strength (1 left click, 0.5 both, 0 right).

Generic Entities

entity.get_by_class(class_name)
→table
Entity indices of a class ("CPlantedC4").
entity.get_class_name(ent_idx)
→string
entity.get_ent_origin(ent_idx)
→number, number, number
World origin of any entity.
local bombs = entity.get_by_class("CPlantedC4")
if #bombs > 0 then
    local x, y, z = entity.get_ent_origin(bombs[1])
    local sx, sy, ok = client.world_to_screen(x, y, z)
    if ok then
        draw.rect_filled(sx - 10, sy - 10, 20, 20, 255, 60, 60, 200)
        draw.text(sx + 14, sy - 6, "BOMB", 255, 60, 60, 255)
    end
end

utils

Tracing, grenade simulation, clipboard, memory and random numbers.

Trace

utils.trace_line(sx,sy,sz, ex,ey,ez, mask?)
→number, integer | nil
Line trace: fraction (0–1) and the hit entity. Default mask 0x1C3003.
utils.trace_hull(sx,sy,sz, ex,ey,ez, minx,miny,minz, maxx,maxy,maxz, mask?)
→number, integer | nil
utils.trace_bullet(sx,sy,sz, ex,ey,ez)
→boolean, number, integer
A bullet of the active weapon (penetration included): hit, damage, hitgroup.

Grenades

utils.simulate_grenade(eye_x, eye_y, eye_z, pitch, yaw, strength?, vel_x?, vel_y?, vel_z?, weapon?)new
→table, boolean, number
Flight of a grenade thrown from that eye with those angles and player velocity, on the CS2 server model (1/128 s steps, collision sphere, bounces, detonation rules). Returns the path ({ {x, y, z}, … }, a point per tick), whether it detonated and the flight time. weapon picks the detonation rule ("weapon_smokegrenade", "weapon_molotov" …).
utils.simulate_throw({ x, y, z, pitch, yaw, duck, run, run_yaw, walk, jump, delay, strength, weapon })new
→table, boolean, number
A whole scripted throw from a standstill at the feet position: run ticks of running along yaw + run_yaw (walking with walk), the jump, the release delay ticks later — through the game's own movement — then the flight as simulate_grenade. Uses the eye and velocity the server throws with (the tick after the release, or the jump's for a jump throw).

Clipboard

utils.clipboard_get()new
→string
"" if empty.
utils.clipboard_set(text)new
→boolean

Low-level & Memory

utils.opcode_scan(module, pattern)
→integer
IDA-style pattern scan, 0 if not found.
utils.create_interface(dll, interface_name)
→integer
utils.get_schema_offset(class_name, field_name)
→integer
utils.get_vfunc(ptr, index)
→integer

Random

utils.random_int(min, max)
→integer
utils.random_float(min, max)
→number
utils.random_seed(seed)
-- where would a smoke thrown right now from here land?
callbacks.register("on_render", function()
    local me = entity.get_local_player()
    if not me then return end
    local ex, ey, ez = client.get_eye_pos()
    local pitch, yaw = client.get_view_angles()
    local vx, vy, vz = entity.get_velocity(me)
    local path, detonated, t = utils.simulate_grenade(ex, ey, ez, pitch, yaw, 1, vx, vy, vz, "weapon_smokegrenade")
    local last = path[#path]
    if last then draw.circle_3d(last[1], last[2], last[3], 16, 120, 200, 255, 255, 1.5) end
end)

json

JSON for script data (location lists, settings, exchange through the clipboard).

json.decode(text)new
→any | nil
Lua tables / values from JSON text, nil on a parse error.
json.encode(value)new
→string
JSON text of a table / value (arrays for sequences, objects otherwise).
local path = client.get_scripts_dir() .. "my_data.json"
local f = io.open(path, "r")
local data = f and json.decode(f:read("a")) or {}
if f then f:close() end
data.runs = (data.runs or 0) + 1
f = io.open(path, "w") f:write(json.encode(data)) f:close()

cmd

The user command being built. Only inside on_pre_move and on_createmove (elsewhere it raises an error).

cmd.command_number()
→integer
cmd.tickcount()
→integer

Movement

cmd.move()
→number, number, number
forwardmove, leftmove, upmove (−1 … 1).
cmd.set_move(forward, left, up?)
Relative to the camera; the cheat's movement fix turns it for the angles that go out.

Angles

cmd.viewangles()
→number, number, number
The command's angles: pitch, yaw, roll.
cmd.viewangles(pitch, yaw, roll?)
Set the command's angles.
cmd.set_view_angles(pitch, yaw, silent?)new
The angles the command (and every input history entry) goes out with — a shot or a grenade flies along them. Not silent: the camera turns too and moves set afterwards are relative to them. Silent: the camera stays.

Buttons

cmd.buttons()
→integer
The button bitmask.
cmd.buttons(button, state?)
Set a button state directly (raw; prefer set_button).
cmd.get_button(mask)new
→boolean
True if any of the buttons is held.
cmd.set_button(mask, down)new
Hold or release buttons with the right edge against the previous command (pressed / held / released / up).
cmd.add_button_step(mask, pressed, when?)new
A subtick button event at when (0–1 of the tick, default 0.5) — what a real key press sends next to the button state. Attack and jump are taken at their step's time: a jump throw is a jump press and an attack release at the same when.

Button constants

IN_ATTACK1left click
IN_JUMP2
IN_DUCK4
IN_FORWARD8W
IN_BACK16S
IN_USE32E
IN_MOVELEFT512A
IN_MOVERIGHT1024D
IN_ATTACK22048right click
IN_RELOAD8192R
IN_SPEED65536Shift (walk)
ℹ
Define the constants you need in your script — e.g. local IN_ATTACK, IN_JUMP = 1, 2.
local IN_ATTACK, IN_JUMP = 1, 2
ui.add_hotkey("Jump throw")

-- a jump throw bind: jump and release the grenade at the same subtick
callbacks.register("on_pre_move", function()
    if not input.is_key_down(ui.get_hotkey("Jump throw")) then return end
    cmd.set_button(IN_JUMP, true)
    cmd.add_button_step(IN_JUMP, true, 0.5)
    cmd.set_button(IN_ATTACK, false)
    cmd.add_button_step(IN_ATTACK, false, 0.5)
end)

movement

Take the movement over from the cheat for a command. Call every command it should hold (in on_pre_move).

movement.block_autostrafe()new
No auto strafe on this command.
movement.block_features()new
This command runs without the cheat's movement: auto strafe, bunny hop, quick stop, slow walk, jump bug, edge bug, air duck, ramp boost, auto peek and super toss — for scripted movement (grenade helpers).

ui

Controls on the script's settings page. Add them at the top level; values are saved with the config.

Controls

ui.add_checkbox(name, default?)
ui.get_bool / set_bool.
ui.add_slider_int(name, min, max, default?)
ui.get_int / set_int.
ui.add_slider_float(name, min, max, default?)
ui.get_float / set_float.
ui.add_combo(name, { options }, default?)new
Index from 0: ui.get_combo / set_combo. Alias add_combobox.
ui.add_color_picker(name, r?, g?, b?, a?)new
Channels 0–1: ui.get_color / set_color.
ui.add_hotkey(name, key?)new
Virtual-key code: ui.get_hotkey / set_hotkey.
ui.add_input(name, default?)new
Text field (127 chars): ui.get_string / set_string.
ui.add_button(name)new
Button: ui.is_clicked.
ui.add_label(text)new
Alias add_text.
ui.add_separator()new
ui.begin_group(name, default_open?)new
Following controls go into a collapsible group until end_group.
ui.end_group()new
ui.clear_group(name)new
→boolean
Remove a group and its controls (to rebuild it).

Values

ui.get_bool(name)
→boolean
ui.get_int(name)
→integer
ui.get_float(name)
→number
ui.get_combo(name)new
→integer
ui.get_color(name)new
→number, number, number, number
ui.get_hotkey(name)new
→integer
ui.get_string(name)new
→string
ui.is_clicked(name)new
→boolean
True once per click.
ui.set_bool(name, value)new
→boolean
The set_* functions return false for an unknown name.
ui.set_int(name, value)new
→boolean
Also set_combo.
ui.set_float(name, value)new
→boolean
ui.set_color(name, r, g, b, a)new
→boolean
ui.set_hotkey(name, key)new
→boolean
ui.set_string(name, value)new
→boolean

Visibility

ui.set_visible(name, visible)new
→boolean
ui.is_visible(name)new
→boolean
ui.set_disabled(name, disabled)new
→boolean
ui.is_disabled(name)new
→boolean
ui.add_checkbox("Enable ESP", true)
ui.add_slider_int("ESP Distance", 100, 5000, 2000)
ui.add_combo("Style", { "Box", "Corners", "Off" }, 0)
ui.add_color_picker("Color", 1, 0.3, 0.3, 1)
ui.add_input("Tag", "nonagon")
ui.add_button("Reset")

callbacks.register("on_render", function()
    if ui.is_clicked("Reset") then ui.set_int("ESP Distance", 2000) end
    ui.set_visible("Color", ui.get_combo("Style") ~= 2)
    if not ui.get_bool("Enable ESP") then return end
    local r, g, b, a = ui.get_color("Color")
end)

cfg

Key-value store shared between scripts. Resets on script reload.

cfg.get(key)
→any | nil
cfg.set(key, value)

antiaim

Control anti-aim from Lua (degrees). Only inside on_createmove; the values reset every tick.

antiaim.set_yaw_offset(degrees)
Add an offset to the anti-aim yaw.
antiaim.override_yaw(degrees)
Replace the yaw, relative to the camera (180 = backwards).
antiaim.override_pitch(degrees)
Force the pitch (−89 … 89).
antiaim.get_real_yaw()
→number
The camera yaw.
antiaim.get_state()new
→boolean, integer, boolean, boolean, boolean, boolean
Enabled, manual state, freestanding, Lua override active, yaw overridden, pitch overridden.
ℹ
Anti-aim switches off by itself on the tick a grenade leaves the hand (and on a jump with the pin pulled, whose angles a jump throw uses).
callbacks.register("on_createmove", function()
    antiaim.set_yaw_offset(math.sin(client.curtime() * 8) * 60)
    antiaim.override_pitch(89)
end)

ragebot

Read the ragebot's state.

ragebot.is_enabled()new
→boolean
ragebot.has_target()new
→boolean
ragebot.get_target()new
→integer, boolean, integer, integer, number, number, number, number, number, number, number
Entity index, valid, damage, hitbox, hitchance, required hitchance, fov, distance, point x, y, z.

Hit Groups

Returned by utils.trace_bullet and on_player_hurt.

0
GENERIC
1
HEAD
2
CHEST
3
STOMACH
4
LEFT ARM
5
RIGHT ARM
6
LEFT LEG
7
RIGHT LEG
8
NECK

Trace Masks

Optional mask for utils.trace_line / trace_hull.

0x1C3003
MASK_SHOT (default)
0x1C300B
MASK_SHOT_PLAYER — includes hitboxes
0x4003
MASK_VISIBLE — line of sight
0x10001
MASK_SOLID — world only

Examples

Damage Predictor

callbacks.register("on_render", function()
    local y = 40
    local lx, ly, lz = client.get_eye_pos()
    for _, idx in ipairs(entity.get_players()) do
        local ex, ey, ez = entity.get_eye_pos(idx)
        local hit, dmg = utils.trace_bullet(lx, ly, lz, ex, ey, ez)
        if hit then
            draw.text(10, y, string.format("[%s] %d dmg | %d hp", entity.get_name(idx), dmg, entity.get_health(idx)), 255, 200, 0, 255)
            y = y + 16
        end
    end
end)

Grenade Spot (walk in, aim, throw)

local IN_ATTACK, IN_JUMP = 1, 2
local spot = { pos = { -300.0, -1163.0, -167.97 }, pitch = -30.0, yaw = 120.0, jump = true }
ui.add_hotkey("Throw")

callbacks.register("on_pre_move", function()
    if not input.is_key_down(ui.get_hotkey("Throw")) then return end
    local me = entity.get_local_player()
    local is_nade, pin = entity.get_grenade_state(me)
    if not is_nade then return end
    movement.block_features()
    cmd.set_view_angles(spot.pitch, spot.yaw, true)   -- silent: the camera stays
    local x, y = entity.get_origin(me)
    local dx, dy = spot.pos[1] - x, spot.pos[2] - y
    if dx * dx + dy * dy > 0.25 then
        local _, cam_yaw = client.get_view_angles()
        local rel = math.rad(math.deg(math.atan(dy, dx)) - cam_yaw)
        cmd.set_move(math.cos(rel), math.sin(rel))      -- walk onto the spot
        cmd.set_button(IN_ATTACK, true)                 -- keep the pin pulled
    elseif pin then
        if spot.jump then cmd.set_button(IN_JUMP, true) cmd.add_button_step(IN_JUMP, true, 0.5) end
        cmd.set_button(IN_ATTACK, false)
        cmd.add_button_step(IN_ATTACK, false, 0.5)
    end
end)

callbacks.register("on_render", function()
    local path = utils.simulate_throw({ x = spot.pos[1], y = spot.pos[2], z = spot.pos[3],
        pitch = spot.pitch, yaw = spot.yaw, jump = spot.jump, weapon = "weapon_smokegrenade" })
    local px, py, pok
    for _, p in ipairs(path) do
        local sx, sy, ok = client.world_to_screen(p[1], p[2], p[3])
        if ok and pok then draw.line(px, py, sx, sy, 120, 200, 255, 255, 1.5) end
        px, py, pok = sx, sy, ok
    end
end)

Find Smoke Grenades

callbacks.register("on_render", function()
    for _, ent in ipairs(entity.get_by_class("CSmokeGrenadeProjectile")) do
        local x, y, z = entity.get_ent_origin(ent)
        draw.circle_3d(x, y, z, 144, 180, 180, 180, 200, 1, 64)
    end
end)