Skip to Content

esp

Register custom ESP Builder elements: element spec, slots, settings, render and value callbacks, element handles, errors and saved settings.


A script adds its own elements to Visuals > Player > ESP Builder. The ESP places them, as it places its own Health bar or Name: a bar stacks with the built-in bars of its side, a row with the side's texts and icons, and the user drags, switches and configures the element in the builder like a built-in one. The script only draws, in a render callback, into the place it was given.

local handle, reason = esp.register_element(spec)

Returns an element handle, or nil and the reason (esp: …). Nothing is registered when the spec is refused. Call it from the script body, a callback or a timer, not from inside an element's own render, value or text function: there it returns nil, "esp: register_element is not available inside an ESP element callback". handle:remove() is allowed anywhere, its own element's function included. The element reaches the builder and the game on the next frame.

Element spec

FieldTypeDefaultMeaning
idstringrequired1–32 characters of a-z, 0-9 and _, unique within the script. The saved id is lua/<file name without .lua>/<id>.
namestringidThe row caption in the builder.
slot'bar', 'row', 'free', 'text''row'Where the layout puts the element (below). 'text' is a value element.
side'left', 'right', 'top', 'bottom''left' for a bar, 'right' otherwiseWhere it starts; the user may drag a bar or a row to another side.
thicknessnumber or setting id2A bar's thickness across its side: pixels above 0, or the id of one of its slider_int / slider_float settings. A value bar takes pixels only: its built-in Thickness setting edits it. Ignored for other slots.
size{ w, h }noneA drawn row's fixed size, both above 0. Without it the row sizes itself from what render returns. Ignored for other slots.
colorcolourwhiteThe element's colour; the builder's colour row edits it.
enabledbooleantrueWhether the element starts switched on.
profilestable of 'enemies', 'local'bothThe ESP profiles the element joins; an empty table is refused.
statestable of state names{ 'behind_wall' }The states the user may give the element a colour for: behind_wall, low_hit_chance, low_ammo, reloading. {} offers none.
settingstable of settingsnoneThe element's own settings (below).
renderfunctionrequired, except for a value elementrender(ctx), called once per element per drawn player per frame, and per builder frame.
valuefunctionnoneA bar's value function, in place of render.
textfunctionrequired for 'text'A text element's text function.

A script registers 16 elements at most, and all scripts together 64. An element taken out with handle:remove() stops counting at the next registration; a disabled one (see Errors) still counts.

slotPlaced bySize
'bar'The bar stack of its side, outside the box, in builder order with the Health and Ammo bars.Thickness across the side, the box's length along it.
'row'The text and icon stack of its side, in builder order with Name, Weapon, Flags and the others.size, or what render returns.
'free'Nothing: the element draws over the box, (0, 0) its top-left corner.The box.

Settings

Each entry of settings becomes a row in the element's settings in the builder. The value is saved with the ESP configuration under the element and is read as ctx.settings.<id>.

FieldApplies toMeaning
idall1–32 characters of a-z, 0-9 and _, unique within the element. Required.
typeall'checkbox', 'slider_int', 'slider_float', 'combo' or 'color_edit'. Required.
nameallRow caption; defaults to id.
defaultallStarting value: boolean, number (a combo's zero-based item index) or colour. Without it: false, the slider's min, the first item, white. A number is clamped into the range or the items.
min, max, stepslidersRange, default 0–100 for slider_int and 0–1 for slider_float. max must be above min.
formatslidersThe value's display format.
itemscomboItem captions, at least one; entries that are not strings are skipped.
wheelslidersThe mouse wheel over the element in the builder moves this setting. One setting per element at most.
pickeralltrue puts the row in the element colour's picker, where the built-in elements keep their look.
parentallThe id of an earlier setting of the element: the row sits in its picker when the parent is a color_edit, behind its ... otherwise. Three levels deep at most; picker and parent exclude each other.

Render callback

render(ctx) draws the element. For a bar or a row it runs before the layout places the element: its ctx: calls are recorded in the element's own coordinates, (0, 0) being the top-left corner of its place, and drawn once that place is known. The callback is called once per drawn player per frame; there is no separate measuring call.

ReturnEffect
nothing (or anything else)A bar, a free element or a row with size is drawn. A row without size takes no place and draws nothing: it must return its size.
falseNothing is drawn and the element takes no place: the next element of the stack moves in.
w, hA row without size takes this size; it must be above 0 both ways. Ignored elsewhere.

ctx fields:

FieldTypeMeaning
ctx.w, ctx.hnumberThe element's place: a bar's thickness and length, a row's size (zero for a self-sizing row), a free element's box.
ctx.sidestring or nil'left', 'right', 'top' or 'bottom' for a bar or a row; nil for a free element.
ctx.verticalbooleantrue on the left and right side, where a bar grows bottom to top.
ctx.colorcolourThe element's colour, with the state colour the player is in already applied (render callbacks only; see Value elements).
ctx.settingstableThe settings by id. Read-only: assigning one raises esp ctx is read-only. pairs(ctx.settings) sees nothing; read the ids you declared.
ctx.playerentity or nilThe player's pawn (Entities); nil in the builder.
ctx.indexnumber or nilThe pawn's entity index; nil in the builder.
ctx.profilestring'enemies' or 'local'.
ctx.behind_wallboolean or nilWhether the player is behind a wall; nil when no element of the profile uses the Behind Wall state, so it was not traced.
ctx.previewbooleantrue in the builder's preview, which calls the same callback with ctx.player = nil. Draw sample values there.
ctx.scalenumberThe overlay's scale: the menu's scale in the builder. ctx:text and ctx:text_size apply it themselves.

ctx is read-only; assigning a field raises. It belongs to its call: using it after the callback returned raises esp ctx used outside its render call.

ctx methods take points as vector2d in the element's coordinates:

ctx:rect(pos, size, color, rounding?)
ctx:rect_outline(pos, size, color, thickness?, rounding?)
ctx:rect_filled_fade(from, to, top_left, top_right, bottom_right, bottom_left)
ctx:line(from, to, color, thickness?)
ctx:circle(center, color, radius, start_degrees?, fraction?)   -- filled disc, or a sector below fraction 1
ctx:circle(center, radius, segments, color, thickness?)        -- outline; segments is ignored, the circle is smooth
ctx:arc(center, radius, from_radians, to_radians, segments, color, thickness?) -- segments is ignored
ctx:polygon(points, color)                                     -- convex fill
ctx:poly_line(points, color, thickness?, closed?)
ctx:text(text, pos, color?, size?, style?)                     -- style = { font = 'ubuntu'|'visitor', effect = 'shadow'|'outline'|'none' }
local w, h = ctx:text_size(text, size?, style?)
ctx:texture(texture, from, to, tint?, rounding?)               -- texture_t from render.setup_texture*
ctx:bar(value, max, options?)                                  -- the built-in bar look over the whole place
local point = ctx:project(world)                               -- vector2d or nil
  • Text defaults to white, size 11 and the ESP's own font with a shadow, as the built-in texts.
  • ctx:bar fills the place with a background, a one-pixel outline and the fill, growing bottom to top on the left and right and left to right on top and bottom. options takes color (default ctx.color), background, outline (default true) and inverted.
  • ctx:project answers a world position in the element's coordinates, or nil when the point cannot be projected. Only a free element knows its place while it draws; for a bar or a row it returns nil.
  • A shape with a non-finite coordinate or an empty size draws nothing.
  • One call records at most 4096 shapes and 64 KiB of text. A ctx:polygon counts one shape per triangle (points minus two), a ctx:poly_line one per segment, a sector one per triangle and ctx:bar up to three. Past that the rest of the call is dropped, what was recorded is drawn, and the error is reported once per load; the element is not disabled.

Value elements

A value element is drawn by the ESP itself, as its Health bar or Name are, with every setting the built-in kind has in the builder: Coloring, gradient, segments, fill style, background, outline, the value label and its font for a bar; font, size, case, effect and plate for a text. The script only supplies the value.

esp.register_element { id = 'shield', slot = 'bar', side = 'right', value = function(ctx) return value, max end }
esp.register_element { id = 'rank', slot = 'text', side = 'bottom', text = function(ctx) return 'Global Elite' end }
  • value(ctx) returns the bar's value and, optionally, its maximum (1 when left out). The label shows the value with no decimals.
  • text(ctx) returns a string.
  • Returning nothing, nil or false draws nothing for that player, and the element takes no place.
  • The function is called once per drawn player per frame while the element is switched on in that player's profile, before the ESP draws; switched off, it is not called. The builder calls it with ctx.preview = true, switched on or off, for each preview player whose profile has the element, when the builder is built and whenever a script's element is registered or taken out; it keeps that answer until the next such call.
  • ctx has the fields above, with w and h 0, side nil, scale 1, and color the element's own colour, before any state colour. Its drawing methods raise, as a value element draws nothing itself.
  • A 'text' element stacks with the texts of its side, a value bar with the bars. A function that raises is reported and disables its element, as a render callback's does.
  • Saved settings follow the element as for every element. A value bar that becomes a text or a drawn element, or the other way round, starts from its defaults in its saved place: the settings of one kind do not fit another.

Errors

A callback that raises is reported with its traceback, as other callback errors are (esp '<id>': … on the Scripts tab, the console and MCP), and that element is not called again until the script reloads. It draws nothing in the frame it raised; the other elements, built-in or a script's, are unaffected.

Element handle

MemberMeaning
handle.idThe element's id.
handle:remove()Takes the element out of the builder and the game on the next frame. Its settings are kept, as on unload. A second call, or a call after the script unloaded, does nothing.
tostring(handle)esp_element<id>. The handle's type is esp_element_t.

Lifetime and saved settings

  • Unloading or reloading the script takes its elements out; their place, switch, colour, state colours and settings stay in the ESP configuration. Registering the same id again, in this session or after a restart, gives them back.
  • A configuration saved or loaded while the script is not loaded keeps them too.
  • Delete on the Scripts tab forgets them. So does the reset of MCP's lua_eval scratch state for the elements a snippet registered there.
  • Changing side or name in the script, or a drawn element's slot, keeps the saved settings: they belong to the id.

Example

An armor bar left of the box, beside the Health bar, and a BOT row for bots.

-- @name Armor bar and BOT tag
-- @description An armor bar beside the health bar and a BOT tag for bots, placed in the ESP Builder.

local FL_CONTROLLER_FAKECLIENT = 256

esp.register_element {
    id = 'armor_bar',
    name = 'Armor bar',
    slot = 'bar',
    side = 'left',
    color = color(80, 160, 255),
    thickness = 'thickness',
    settings = {
        { id = 'thickness', type = 'slider_float', name = 'Thickness', min = 1, max = 8, default = 2, wheel = true, picker = true },
        { id = 'hide_empty', type = 'checkbox', name = 'Hide without armor', default = true },
        { id = 'background', type = 'color_edit', name = 'Background', default = color(0, 0, 0, 128), parent = 'hide_empty' },
    },
    render = function(ctx)
        local armor = ctx.preview and 65 or ctx.player.m_ArmorValue
        if armor <= 0 and ctx.settings.hide_empty then
            return false -- no place: the next bar moves in
        end
        ctx:bar(armor, 100, { background = ctx.settings.background })
    end,
}

esp.register_element {
    id = 'bot_tag',
    name = 'BOT tag',
    slot = 'row',
    side = 'right',
    profiles = { 'enemies' },
    color = color(255, 200, 80),
    settings = {
        { id = 'size', type = 'slider_float', name = 'Size', min = 8, max = 16, default = 11, wheel = true },
        { id = 'outline', type = 'checkbox', name = 'Outline', default = false },
    },
    render = function(ctx)
        if not ctx.preview then
            local controller = ctx.player:controller()
            if not controller or bit.band(controller.m_fFlags, FL_CONTROLLER_FAKECLIENT) == 0 then
                return false -- not a bot: no row, no gap
            end
        end
        local style = { effect = ctx.settings.outline and 'outline' or 'shadow' }
        local w, h = ctx:text_size('BOT', ctx.settings.size, style)
        ctx:text('BOT', vector2d(0, 0), ctx.color, ctx.settings.size, style)
        return w, h -- the row's size: the layout stacks it with the other right-side rows
    end,
}

The same two as value elements: the ESP draws them with the built-in bar's and text's settings, which the builder edits.

-- @name Armor and BOT, built-in look
-- @description The armor as a built-in bar and BOT as a built-in text, styled in the ESP Builder.

local FL_CONTROLLER_FAKECLIENT = 256

esp.register_element {
    id = 'armor_value',
    name = 'Armor',
    slot = 'bar',
    side = 'left',
    color = color(80, 160, 255),
    value = function(ctx)
        local armor = ctx.preview and 65 or ctx.player.m_ArmorValue
        if armor <= 0 then
            return nil -- no bar, no gap
        end
        return armor, 100
    end,
}

esp.register_element {
    id = 'bot_text',
    name = 'BOT',
    slot = 'text',
    side = 'right',
    profiles = { 'enemies' },
    color = color(255, 200, 80),
    text = function(ctx)
        if ctx.preview then
            return 'BOT'
        end
        local controller = ctx.player:controller()
        return controller and bit.band(controller.m_fFlags, FL_CONTROLLER_FAKECLIENT) ~= 0 and 'BOT' or nil
    end,
}

A value bar, a value text and a drawn element with its own settings together, ready to load: features/lua/examples/esp_elements.lua.

On this page