GriefBot

Lua scripting

Write your own overlays in Lua: a few lines that draw on screen, react to the match and keep their own settings in the menu. Everything a script can do is on this page.

Get started

  1. Open the menu and go to Scripts.
  2. Type a name into New script and press Create. GriefBot writes a short starter script, switches it on and opens it in your editor.
  3. Change it and save. A script that is switched on reloads every time its file is saved, so you see each change in game straight away.
  4. Anything you print, and the full error when a script stops, shows in Output under the scripts.

Every script is one .lua file in your cfg\scripts folder (Open folder on the Scripts tab takes you there), and its first line must be:

-- GriefBot script

This is the whole starter script. It adds two settings to its card in the menu and draws one line of text every frame:

-- GriefBot script
local show  = ui.toggle("Show", true)
local color = ui.color("Colour", "#5aa5ff")

events.on("paint", function()
    if not show:get() then return end
    local w = client.screen()
    render.text(w / 2, 48, "Hello from Lua", color:get(), 16, "c")
end)

Scripts use Lua 5.4. If you have never written Lua, the first chapters of Programming in Lua cover everything these pages use.

How a script runs

When a script is switched on, its file runs once from top to bottom. That is where it declares its settings and registers its callbacks with events.on. After that, GriefBot calls the callbacks:

  • "paint" once every frame. Draw here; drawing anywhere else does nothing.
  • A game event such as "player_death" when the game fires it, just before the next paint.
  • "unload" once when the script is switched off, reloaded or the overlay unloads.
CallWhat it does
events.on(name, function)Calls function for name: "paint", "unload" or a game event. Call it as often as you like; every function registered for a name runs, in the order they were added. A game event's function receives one table with the event's fields.

Settings can only be declared while the file runs the first time, not from inside a callback. Variables at the top of the file keep their values between callbacks, and are reset when the script reloads; use storage for anything that must survive that.

Everything a script does happens on the overlay's own frame, so a slow script costs frames. The card on the Scripts tab shows how many milliseconds each script takes a frame.

Settings

A script's settings appear in its card's cog on the Scripts tab, drawn like every other setting in the menu, and are saved for you. Each call returns a handle; read it with :get() whenever you need the current value, because the user can change it at any time.

DeclareWhat it does
ui.toggle(label, default)An on/off switch. default is false if left out. :get() returns a boolean.
ui.slider(label, min, max, default, format)A slider for a number. default is min if left out. format is how the value is printed, with one number in it: "%.1f" (the default), "%.0f px", "%.2f s".
ui.slider_int(label, min, max, default)A slider for a whole number. :get() returns an integer.
ui.combo(label, options, default)A dropdown. options is a list of 1 to 16 strings and default the position of the selected one, starting at 1. :get() returns that position and :item() its text.
ui.color(label, default)A colour picker with transparency. :get() returns a colour.
ui.key(label, default)A key the user binds, with a mode: hold, toggle, hold to turn off, or always. default is a key or nothing. :get() returns true while the key has it active.
HandleWhat it does
handle:get()The current value.
handle:set(value)Changes the value from the script, as if the user had. Not for ui.key: the user binds keys.
handle:item()For ui.combo only: the text of the selected option.

A label is 1 to 47 characters with no ##, = or line break, and is unique within its script. A script has at most 32 settings. Values are remembered by label, so renaming a setting starts it from its default.

local size  = ui.slider_int("Text size", 10, 24, 14)
local side  = ui.combo("Side", { "Left", "Right" }, 1)
local key   = ui.key("Hold to hide", "mouse4")

events.on("paint", function()
    if key:get() then return end
    local w = client.screen()
    local x = side:get() == 2 and w - 200 or 40
    render.text(x, 300, "Side: " .. side:item(), "#ffffff", size:get())
end)

Drawing

Draw from "paint". Positions are screen pixels with 0, 0 at the top left; client.screen() gives the size. Shapes are drawn over the game and under the menu, in the order you draw them. Arguments shown after the colour can be left out.

CallWhat it does
render.line(x1, y1, x2, y2, color, thickness)A line. thickness is 1 if left out.
render.rect(x, y, w, h, color, rounding, thickness)A rectangle outline. rounding is the corner radius.
render.rect_filled(x, y, w, h, color, rounding)A filled rectangle.
render.rect_gradient(x, y, w, h, top_left, top_right, bottom_right, bottom_left)A filled rectangle blending between four corner colours. Left out, top_right is top_left, bottom_right is top_right and bottom_left is top_left, so two colours make a left-to-right fade.
render.circle(x, y, radius, color, thickness, segments)A circle outline. segments is worked out from the size if left out.
render.circle_filled(x, y, radius, color, segments)A filled circle.
render.triangle_filled(x1, y1, x2, y2, x3, y3, color)A filled triangle.
render.polyline(points, color, thickness, closed)Lines through points, a flat list { x1, y1, x2, y2, ... } of 2 to 256 points. closed joins the last point back to the first.
render.polygon(points, color)A filled shape through 3 to 256 points. The shape must be convex.
render.text(x, y, text, color, size, flags)Text with a dark outline. size is in pixels, 6 to 96, 14 if left out. flags is a string of letters: c centres it on x, r ends it at x, m centres it on y and p draws it plain, without the outline.
render.text_size(text, size)Returns the width and height the text takes up.
render.world_to_screen(x, y, z)Where a point in the game world is on screen: returns x, y, or nil when the point is behind the camera or off screen. Also takes a table with x, y and z, such as a player's origin.

Colours

A colour is either a hex string, "#ff8800" or with transparency "#ff8800cc", or the number render.color and ui.color return. Treat that number as a value to pass on, not one to do arithmetic with.

CallWhat it does
render.color(r, g, b, a)A colour from 0 to 255 channels. a is 255 (solid) if left out.

A script can draw up to 20,000 shapes a frame.

Players and the bomb

CallWhat it does
entities.players()Every player in the match, as a list of player tables. Calling it again in the same frame returns the same list, so it costs nothing to call from several places.
entities.get(index)The player with this index, or nil. Game events name players by index.
entities.local_player()You, or nil when you are not in a match.
entities.bomb()The planted bomb, or nil when no bomb is planted.

A player

FieldWhat it does
indexThe player's number in this match. Stays the same while they are connected.
name, steamidTheir name, and their SteamID64 as a string (empty for bots).
team2 for Terrorists, 3 for Counter-Terrorists.
enemy, is_localWhether they are on the other team (deathmatch counts everyone), and whether this is you.
alive, health, armor, helmetAlive or not, health 0 to 100, armour 0 to 100, and whether they have a helmet.
moneyTheir cash.
weapon, weapon_id, clipThe weapon in their hands by name ("ak47", "awp"), its item number, and the rounds in its magazine.
scoped, defusing, planting, reloading, walkingWhat they are doing right now.
has_bomb, has_defuser, flashedCarrying the bomb, carrying a defuse kit, blinded by a flashbang.
dormantThe game has stopped sending this player's position, so the values are their last known ones.
placeThe callout where they are standing, such as "BombsiteA".
origin, eye, head, velocityTables with x, y, z: where their feet are, their eye position, their head, and how fast they are moving in units a second.

The bomb

FieldWhat it does
site"A" or "B".
ticking, time_leftWhether the timer is running, and the seconds until it explodes.
defusing, defuse_left, defusedWhether someone is defusing it, the seconds until they finish, and whether it has been defused.
originWhere it is, as a table with x, y, z. Missing when the position is not known.

Game events

Register a function with events.on and it receives a table with the event's name and the fields below. A field that names a player holds that player's index (look them up with entities.get), or nil when the player cannot be found, such as a bot that just left.

events.on("player_death", function(e)
    local me = entities.local_player()
    if me and e.attacker == me.index and e.headshot then
        print("Headshot with the " .. e.weapon)
    end
end)
EventWhat it does
player_hurtuserid, attacker (players), health and armor left, dmg_health, dmg_armor, hitgroup (1 head, 2 chest, 3 stomach, 4 and 5 arms, 6 and 7 legs), weapon.
player_deathuserid, attacker, assister (players), weapon, headshot, noscope, thrusmoke, attackerblind, penetrated (walls the bullet went through), distance.
weapon_fireuserid, weapon, silenced.
bullet_impactuserid, and where the bullet hit: x, y, z.
player_blinduserid, attacker, blind_duration in seconds.
item_purchaseuserid, team, weapon.
round_starttimelimit, fraglimit, objective.
round_freeze_endThe buy time is over and players can move. No fields.
round_endwinner (team), reason, message.
round_mvpuserid, reason.
bomb_planteduserid, site (the game's own number for the site; entities.bomb().site has the letter).
bomb_begindefuseuserid, haskit.
bomb_defused, bomb_explodeduserid, site.
smokegrenade_detonate, molotov_detonateuserid, and where it went off: x, y, z.
inferno_expireA fire burnt out at x, y, z.
vote_castuserid, vote_option, team.
player_disconnectname, reason.
game_endwinner (team).
cs_win_panel_matchThe match is over and the scoreboard is up. No fields.

Numbers are numbers, flags such as headshot are booleans, and weapon is the game's own name for the weapon.

Client and keys

CallWhat it does
client.screen()The screen's width and height in pixels.
client.in_game()true while you are in a match.
client.map()The map's name, such as "de_mirage", or "" outside a match.
client.menu_open()true while the GriefBot menu is open.
client.ping()Your ping in milliseconds.
client.time()Seconds on a clock that never jumps, for timing things: subtract two readings.
client.curtime()The game's own clock, in seconds.
client.frametime()How long the last frame took, in seconds.

Keys

CallWhat it does
input.key_down(key)true while the key is held.
input.key_pressed(key)true on the one frame the key went down.
input.cursor()The mouse position on screen: x, y.

A key is a name or a Windows virtual-key code from 1 to 255. Names are a letter "a" to "z", a digit "0" to "9", "f1" to "f24", or one of: mouse1 to mouse5, shift, ctrl, alt, lshift, rshift, lctrl, rctrl, lalt, ralt, space, enter, tab, escape, backspace, capslock, insert, delete, home, end, pageup, pagedown, up, down, left, right.

Keys read as up while the menu is open or CS2 is not the window in front, so a script never sees what you type into the menu or another program.

Storage

Values a script wants to keep between reloads and restarts: a counter, a position, a last-seen time. They are saved for you a moment after they change.

CallWhat it does
storage.get(key, default)The stored value, or default (or nil) when there is none.
storage.set(key, value)Stores a number, boolean or string. Setting nil removes the key.

A key is 1 to 48 characters with no = or line break. A script keeps up to 64 values, and a string can be up to 512 bytes. To keep a table, keep its parts under their own keys.

local kills = storage.get("kills", 0)

events.on("player_death", function(e)
    local me = entities.local_player()
    if me and e.attacker == me.index and e.userid ~= me.index then
        kills = kills + 1
        storage.set("kills", kills)
    end
end)

The sandbox

Scripts can draw and read the game. They cannot touch your files, start programs, reach the network, or change what the game or the rest of GriefBot does. That is what makes it safe to run a script somebody else wrote.

What Lua gives you

  • string, table, math, utf8 and coroutine, complete except string.dump.
  • The basic functions: print, pairs, ipairs, type, tostring, tonumber, select, pcall, error, setmetatable and the rest, without dofile, loadfile and require. load takes text only.
  • os.time(), os.date(format, time) and os.clock() (seconds since the script started). No other part of os, and no io or debug.
  • collectgarbage with "count", "collect" or "step".
  • print(...) writes to the Output panel, the values separated by two spaces.

Limits

LimitValue
Memory per script64 MB
Work when the file first runsabout 30 million Lua instructions
Work per callbackabout 5 million instructions for each paint and each event
Shapes per frame20,000 per script
Settings32 per script
Stored values64 per script, strings up to 512 bytes
Scripts64 in the folder

A script that goes past a limit is stopped and says which one. pcall cannot catch running out of time: an endless loop is stopped even inside it.

When a script fails

A script that hits an error stops. Its card turns red and shows the line and the message, such as Line 12: attempt to index a nil value, and the Output panel has the full message with the chain of calls that led to it. The other scripts keep running.

Fix the file and save it: a script that is switched on starts again by itself. Reload in the card's cog runs it again without a change.

The most common error is reading a field of something that is nil, for example entities.local_player().health outside a match. Check first:

local me = entities.local_player()
if not me then return end

Sharing scripts

The Library on the Config tab has a Scripts section with scripts other members published. Get downloads one into your scripts folder, switched off, under a name that does not replace anything you have. Read it first if you like, then switch it on from the Scripts tab.

To publish your own, open its card's cog and choose Publish. You name it and describe it, and the file is published as it is saved. Your account name is shown with it. A published script must start with -- GriefBot script and be at most 256 KB.

Signed-in members can also browse the scripts on the website, in the library.

Examples

Each one is a complete script: paste it into a new file and save.

Speed

-- GriefBot script
local size = ui.slider_int("Size", 12, 32, 18)

events.on("paint", function()
    local me = entities.local_player()
    if not me or not me.alive then return end
    local v = me.velocity
    local speed = math.floor(math.sqrt(v.x * v.x + v.y * v.y))
    local w, h = client.screen()
    render.text(w / 2, h - 160, speed .. " u/s", speed > 250 and "#7ee08a" or "#ffffff", size:get(), "c")
end)

Bomb timer

-- GriefBot script
events.on("paint", function()
    local b = entities.bomb()
    if not b or not b.ticking then return end
    local text = string.format("%s  %.1f s", b.site, b.time_left)
    if b.defusing then
        text = text .. string.format("  -  defused in %.1f s", b.defuse_left)
    end
    local w = client.screen()
    render.text(w / 2, 90, text, b.time_left < 10 and "#ff6b71" or "#ffffff", 20, "c")
end)

Hit log

-- GriefBot script
local hold   = ui.slider("Show for", 1, 10, 4, "%.0f s")
local groups = { "head", "chest", "stomach", "left arm", "right arm", "left leg", "right leg" }
local lines  = {}

events.on("player_hurt", function(e)
    local me = entities.local_player()
    if not me or e.attacker ~= me.index then return end
    local victim = entities.get(e.userid)
    table.insert(lines, {
        text = string.format("Hit %s in the %s for %d (%d left)",
            victim and victim.name or "someone", groups[e.hitgroup] or "body", e.dmg_health, e.health),
        at = client.time(),
    })
end)

events.on("paint", function()
    local now = client.time()
    for i = #lines, 1, -1 do
        if now - lines[i].at > hold:get() then table.remove(lines, i) end
    end
    local y = 200
    for _, line in ipairs(lines) do
        render.text(24, y, line.text, "#e0e6f0", 14)
        y = y + 18
    end
end)

Health bars

-- GriefBot script
local color = ui.color("Bar", "#7ee08a")

events.on("paint", function()
    for _, p in ipairs(entities.players()) do
        if p.enemy and p.alive and not p.dormant then
            local tx, ty = render.world_to_screen(p.head.x, p.head.y, p.head.z + 10)
            local bx, by = render.world_to_screen(p.origin)
            if tx and bx then
                local h = by - ty
                local x = tx - h * 0.3 - 6
                local filled = h * p.health / 100
                render.rect_filled(x, ty, 3, h, "#00000099")
                render.rect_filled(x, ty + h - filled, 3, filled, color:get())
            end
        end
    end
end)