Skip to Content

styles

Register custom looks for HUD elements: style spec, element functions and their row data, the style ctx, style settings, errors, lifetime and style handles.


A script gives a HUD element a look of its own. The element keeps every decision - which rows it shows, in which order, with which tone, numbers and timing - and hands the style a read-only snapshot of them each frame; the style draws it. The user picks a style per element: in the right-click menu of the element's HUD window (Style) and in the element's own settings ("..."). Without a style chosen, or while it cannot draw, the element draws its built-in look.

local handle, reason = styles.register(spec)

Returns a style handle, or nil and the reason (styles: …). Call it from the script body, a callback or a timer; inside a style function it returns nil, "styles: register is not available inside a style function". The style reaches the Style lists on the next frame.

Style spec

FieldTypeMeaning
idstringRequired: 1–32 characters of a-z, 0-9 and _, unique within the script. The saved id is lua/<file name without .lua>/<id>.
namestringThe caption in the Style lists; defaults to id.
settingstableThe style's own settings (below).
<element>functionOne per element the style draws, keyed by its name: indicators = function(ctx, data) ... end. The keys present are the elements the style supports; at least one is required. A function under a key that names no element is refused (styles: unknown element 'radar'), so a style written for a newer build fails loudly.

styles.elements lists the element names this build draws: indicators, status_bar, spectator_list, hit_log, proximity_warning.

A script registers 8 styles at most, and all scripts together 32; a removed style stops counting at the next registration.

The reasons a spec is refused, each after the styles: prefix, in the order they are checked: register is not available inside a style function, a script registers 8 styles at most, the scripts register 32 styles at most, id is 1-32 characters of a-z, 0-9 and _, style '<id>' is already registered, unknown element '<name>', a style draws at least one element (indicators = function(ctx, data) ... end), then a settings reason or one of Style settings' own. Only string keys holding functions name elements; other keys are ignored. A spec that is not a table raises.

Element functions

fn(ctx, data) draws the element's whole window, (0, 0) its top-left corner, in the HUD's reference pixels (1080p; the HUD scales them), and returns its size: return w, h (both above 0). The HUD window places, snaps, frames and drags a block of that size, exactly as it does the built-in look. Returning false draws nothing this frame; any other return draws nothing and is reported once per load. Drawing outside the returned box is not clipped.

The function runs once per frame while the element is on and the style is chosen for it, and while the menu is open with nothing live it runs with the element's built-in preview data (ctx.preview is true) and draws at the preview's opacity. Switched off, the element calls nothing.

data.items are the rows in reading order, top to bottom, as the built-in look shows them at the window's anchor (a window anchored to the bottom grows upward, so its first row arrives last). The tables are reused frame to frame; read them during the call. data.items is set again before each call and holds only that call's rows, so a style that replaces or extends it sees the element's rows again next time. Every row carries:

FieldMeaning
alpha0..1: the row easing in when it appears and out after it is gone, as the built-in rows do.
ageSeconds since the row first showed.
leavingtrue while a row the element no longer lists fades out; it is still in data.items.

indicators rows: id (safe_mode, bomb, defuse, jump_bug, slow_walk, auto_peek, duck_peek, double_tap, min_damage, force_shot), key, icon (one glyph of the HUD icon font, draw it with { font = 'icons' }; '' for none), label, value, slab (strings, '' when absent), number (the figure value shows: bomb and defuse seconds, Min Damage; or nil), tone (accent, danger, warning, success, active, muted, strong), color (the tone in the HUD palette), progress (0..1 or nil), pulse, active (false for a dimmed bind; with Hide Inactive on, live ones never arrive, while the preview and a row fading out may still show them).

status_bar rows, one per cell in the user's Items order, left to right wherever the bar sits: id (brand, user, ping, fps, cpu, clock), text (the brand, the licence name, HH:MM; '' for a reading), value (a reading's number, or nil), unit (MS, FPS, CPU; '' for a text cell), suffix (% after CPU, + past 999 FPS, else ''), fraction (0..1, the meter's fill, or nil), verdict (good, fair, poor; nil for CPU and the text cells) and verdict_color. The cells neither enter nor leave: alpha is 1 and age 0. While the menu is open over a bar with nothing selected, the preview is the brand cell.

spectator_list rows, the viewers in arrival order: key, name, initial (the placeholder letter for a player without a Steam account, such as a bot; '' otherwise), avatar (the Steam picture once it has loaded, else nil; draw it with ctx:texture), mode (eye: first person; chase: third person) and is_local (the local player, watching after death). data.target is who they watch: { name = ..., is_local = ... } (is_local: the local player alive and watched; name may be ''). The preview is one row, the local player: the name convar and the current Steam account's avatar (initial '', mode eye).

hit_log rows, newest nearest the window's anchor (first at a top anchor): key, kind (hit dealt, hurt taken, miss, mismatch), name (the target or the attacker), hitgroup (head, chest, …; nil for none), damage and health_left (nil for a miss), hit_chance (a Rage line's, or nil), reason (a miss's or a mismatch's, as the screen words it, or nil), planned ({ hitgroup = ..., damage = ..., hit_chance = ... } for a miss or a mismatch, else nil), parts ({ { text = ..., role = 'text' | 'strong' | 'emphasis' | 'muted' }, ... }: the built-in line's wording and the role each piece is coloured by), text (the parts joined), icon, tone, color (as the built-in line), life (seconds on screen from arrival: 5). A line arrives with age = 0, eases in over the HUD's entry animation, is leaving from age >= life while it eases out, then leaves; 8 lines at most, past that the oldest leaves early. A style with its own timing reads age and life and ignores alpha. The preview is three hits and a miss.

proximity_warning is a point element: fn(ctx, warning) runs once per warning per frame - every warning the Grenade ESP's Proximity Warning would draw, after its Max Distance, Damage and Offscreen decide which exist and where - with (0, 0) at the place the built-in timer would sit: the landing point or the fire on the screen, or its place at the screen's edge. Nothing to return; false hides that warning. The style is chosen in the warning colour's picker (Style, last), its settings under it. warning: kind (he, flash, molotov, incendiary), source (grenade: a predicted landing; fire: a burning inferno), seconds and fraction (the countdown, or nil), threat (nil with Damage off and for a flashbang, else { level = 'safe' | 'caution' | 'danger', caption = ..., damage = ... }: the caption the built-in timer shows and the whole points it names), distance (metres from the eye), offscreen and heading (degrees clockwise from up towards the warning, nil on the screen), icon (the game's HUD icon for the grenade once it has loaded, a hud_texture_t for this call, or nil) and icon_aspect. ctx.color is the picker's colour, ctx.options { damage, offscreen }, and ctx.reach the room in reference pixels around (0, 0) that the edge placement keeps on the screen (56). There is no preview: a warning is pinned to the world. 32 warnings per frame go through styles; past that they draw built-in.

A point style has one more method: ctx:builtin() draws the element's built-in look (the timer, with its arrow off the screen) at its place in the call's drawing - under what is drawn after it, over what was drawn before. In a list element's call it raises ctx:builtin is proximity_warning's.

Style ctx

FieldMeaning
ctx.elementThe element name.
ctx.preview, ctx.editingPreview data; the menu is open.
ctx.anchorThe window's anchor: top_left, top, top_right, left, center, right, bottom_left, bottom, bottom_right.
ctx.alignleft, center or right: how rows align at that anchor.
ctx.time, ctx.frame_timeThe HUD's clock in seconds (item.age counts in it) and the last frame's length.
ctx.scaleScreen pixels per reference pixel. 1 / ctx.scale is one device pixel.
ctx.settingsThe style's settings for this element, by id. Read-only: assigning one, or a ctx field, raises style ctx is read-only; pairs(ctx.settings) sees nothing.
ctx.paletteThe live HUD palette: accent, danger, warning, success, active, muted, strong, background, text, text_muted.
ctx.colorThe element's own colour where it has one, else ctx.palette.accent.
ctx.options, ctx.colorsThe element's display switches and its colours: indicators options.hide_inactive; status_bar options.compact, options.glass, colors.accent, colors.background, colors.text (and ctx.color is the bar's Accent); proximity_warning options.damage, options.offscreen (and ctx.color is the picker's colour).
ctx.reachproximity_warning only: the room in reference pixels around (0, 0) that the edge placement keeps on the screen (56); nil otherwise.

ctx belongs to its call: a method called afterwards raises style ctx used outside its draw call. Its methods are the ESP ctx's (ctx:rect, ctx:rect_outline, ctx:rect_filled_fade, ctx:line, ctx:circle, ctx:arc, ctx:polygon, ctx:poly_line, ctx:text, ctx:text_size, ctx:texture; see Render callback), with lengths, sizes and text sizes in reference pixels. Text defaults to the HUD's face (Plex Sans) at size 12 with no effect; style.font takes the same names as on an ESP ctx. ctx:bar and ctx:project are the ESP's and raise here (ctx:bar is an ESP element's, not a style's). One call records at most 4096 shapes and 64 KiB of text, as an ESP element's; all style calls of a frame together at most 16384 shapes - a call that would take the frame past that draws the built-in look, and so does every later call that frame, reported once.

A style has one method of its own:

ctx:backdrop(pos, size, rounding?)

The HUD's backdrop blur (the one behind its own pills) shows through that rectangle, under everything the call draws, in one blur pass with the call's other backdrops; draw a translucent fill over it for a frosted panel. While the window is a preview, the blur fades with it. A call asks for 8 backdrops at most and a frame for 16 over every style; past that a backdrop draws no blur. On an ESP element's ctx, backdrop and builtin raise (ctx:backdrop is a style's, not an ESP element's).

ctx:texture draws a script's own texture_t and a host texture a style is handed (item.avatar, of type hud_texture_t). A host texture draws only in the call that handed it: one kept for a later call raises a host texture draws only in the call that handed it.

Style settings

The ESP settings schema without picker and a slider's wheel (true for either is refused: styles: setting '<id>': wheel and picker are the ESP's), plus elements = { 'indicators' }: the elements the setting applies to, every element the style draws by default (styles: setting '<id>': elements is a table of element names, styles: setting '<id>': elements names an element the style does not draw); a setting with a parent applies to no element its parent does not (styles: setting '<id>': its elements must be among its parent's). A style takes 64 settings at most (styles: a style takes 64 settings at most). Each element keeps its own values. The rows show under the Style row's "...", in the window's right-click menu and in the element's settings; for proximity_warning, under the Style row of the warning colour's picker.

Errors and lifetime

  • A function that raises is reported with its traceback as style '<id>' (<element>): …, and that element draws its built-in look from the same frame until the script reloads; the Style list marks the style (error). Other elements using the style are unaffected. The Hit Log's lines on screen go to the built-in log with the life they have left.
  • The choice and the settings are saved with the profile under the element. Unloading or reloading the script, or handle:remove(), draws the built-in look and keeps them: the list shows the style as (not loaded), and registering the same id again, in this session or after a restart, gives them back. Delete on the Scripts tab forgets them; the element returns to Built-in. Each element keeps the choices and settings of its 16 most recently chosen styles.

Style handle

MemberMeaning
handle.idThe style's id.
handle:remove()Takes the style out of the lists and the screen; its choice and settings are kept, as on unload. A call after the same id was registered again does nothing: a handle removes only the registration it came from.
tostring(handle)hud_style<id>. The type is hud_style_t.

Ready to load: a pill look for Indicators, features/lua/examples/hud_style_pill.lua; a titled panel for the Spectator List, the Status Bar and Indicators with a Blur setting, features/lua/examples/hud_style_compact_panel.lua; Hit Log lines that slide in and out on their own timing, features/lua/examples/hud_style_slide_hitlog.lua; and a dot for each Proximity Warning, optionally over the built-in timer, features/lua/examples/hud_style_dot_proximity.lua.

On this page