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
| Field | Type | Default | Meaning |
|---|---|---|---|
id | string | required | 1–32 characters of a-z, 0-9 and _, unique within the script. The saved id is lua/<file name without .lua>/<id>. |
name | string | id | The 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' otherwise | Where it starts; the user may drag a bar or a row to another side. |
thickness | number or setting id | 2 | A 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 } | none | A drawn row's fixed size, both above 0. Without it the row sizes itself from what render returns. Ignored for other slots. |
color | colour | white | The element's colour; the builder's colour row edits it. |
enabled | boolean | true | Whether the element starts switched on. |
profiles | table of 'enemies', 'local' | both | The ESP profiles the element joins; an empty table is refused. |
states | table 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. |
settings | table of settings | none | The element's own settings (below). |
render | function | required, except for a value element | render(ctx), called once per element per drawn player per frame, and per builder frame. |
value | function | none | A bar's value function, in place of render. |
text | function | required 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.
slot | Placed by | Size |
|---|---|---|
'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>.
| Field | Applies to | Meaning |
|---|---|---|
id | all | 1–32 characters of a-z, 0-9 and _, unique within the element. Required. |
type | all | 'checkbox', 'slider_int', 'slider_float', 'combo' or 'color_edit'. Required. |
name | all | Row caption; defaults to id. |
default | all | Starting 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, step | sliders | Range, default 0–100 for slider_int and 0–1 for slider_float. max must be above min. |
format | sliders | The value's display format. |
items | combo | Item captions, at least one; entries that are not strings are skipped. |
wheel | sliders | The mouse wheel over the element in the builder moves this setting. One setting per element at most. |
picker | all | true puts the row in the element colour's picker, where the built-in elements keep their look. |
parent | all | The 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.
| Return | Effect |
|---|---|
| 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. |
false | Nothing is drawn and the element takes no place: the next element of the stack moves in. |
w, h | A row without size takes this size; it must be above 0 both ways. Ignored elsewhere. |
ctx fields:
| Field | Type | Meaning |
|---|---|---|
ctx.w, ctx.h | number | The 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.side | string or nil | 'left', 'right', 'top' or 'bottom' for a bar or a row; nil for a free element. |
ctx.vertical | boolean | true on the left and right side, where a bar grows bottom to top. |
ctx.color | colour | The element's colour, with the state colour the player is in already applied (render callbacks only; see Value elements). |
ctx.settings | table | The settings by id. Read-only: assigning one raises esp ctx is read-only. pairs(ctx.settings) sees nothing; read the ids you declared. |
ctx.player | entity or nil | The player's pawn (Entities); nil in the builder. |
ctx.index | number or nil | The pawn's entity index; nil in the builder. |
ctx.profile | string | 'enemies' or 'local'. |
ctx.behind_wall | boolean or nil | Whether the player is behind a wall; nil when no element of the profile uses the Behind Wall state, so it was not traced. |
ctx.preview | boolean | true in the builder's preview, which calls the same callback with ctx.player = nil. Draw sample values there. |
ctx.scale | number | The 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:barfills 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.optionstakescolor(defaultctx.color),background,outline(defaulttrue) andinverted.ctx:projectanswers a world position in the element's coordinates, ornilwhen the point cannot be projected. Only a free element knows its place while it draws; for a bar or a row it returnsnil.- 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:polygoncounts one shape per triangle (points minus two), actx:poly_lineone per segment, a sector one per triangle andctx:barup 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 (1when left out). The label shows the value with no decimals.text(ctx)returns a string.- Returning nothing,
nilorfalsedraws 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. ctxhas the fields above, withwandh0,sidenil,scale1, andcolorthe 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
| Member | Meaning |
|---|---|
handle.id | The 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
idagain, 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_evalscratch state for the elements a snippet registered there. - Changing
sideornamein the script, or a drawn element'sslot, 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.
