Developer reference
CS2 Lua API
HOST 2.4 UI 1.1 · Lua 5.4.7
Build custom ESP, command frameworks, inventory tools and interfaces with the same API used by the bundled scripts.
No matching topics. Try entity.get_local_player or clear the search.
Start here
Download MarkdownFirst script
Create a script from Scripts, paste this example in the editor, and choose Run Lua. Run becomes Unload while the script is active. Open folder and Refresh are in the manager's Other group. Script UI opens custom script pages.
A toggleable CS2 overlay
local enabled = features.add {
id = "health", label = "My health overlay", default = true, key = "F8"
}
ui.tab("controls", "My overlay", function()
ui.feature(enabled)
ui.keybind(enabled, "Toggle key")
end)
ui.overlay("health_overlay", function()
local player = entity.get_local_player()
if not features.active(enabled) or not player or not player.alive then return end
render.text(24, 120, player.name .. " " .. player.health .. " HP",
render.theme().accent, 18)
end)
Choose the correct API
CS2 uses host API 2.4 and UI API 1.1 on Lua 5.4.7. Player snapshots and projections can be unavailable during a map change; check for nil. Settings IDs come from settings.list(). CS2 implements its own ESP color settings; shared esp_colors integration is not available. Aimware CSGO scripts need a manual port to these APIs.
Template controls
Download a template below or use its bundled copy in Scripts. Standalone tools opens with F9 and Visual Studio with F10. Both offer an Open / close key setting and Save preferences. Hidden windows keep their selected overlays running. Unload removes script windows, callbacks and hotkeys. Changes to native CS2 settings persist.
CS2 host API
Download MarkdownCS2 v2 Lua API 2.4
Open Other → Scripts. The visible tabs are Scripts, Lua Editor and Console. The manager opens custom Script UI pages. Toggle API docs in the editor to attach its searchable documentation window to the right of the menu, with expandable signatures and examples.
Getting started
Scripts live in C:\scooby\CS2-v2\scripts. First use installs the CS2 examples: Welcome, Player labels, Session monitor, Saved preferences, and Geometry HUD, Crosshair, and Speed HUD, plus Command framework, Download asset, Inventory studio, Framework - Player ESP and Framework - Asset downloads. The Native hooks template demonstrates pattern discovery, a typed hook, shared values and a custom page; it stays inactive until its pattern is configured. It also installs canonical Custom UI and CS2 adaptations of the Simple-base UI-v2 Standalone Menu and ImGui Demo layouts. Builds refresh the shared docs/templates automatically. Startup upgrades untouched bundled examples and preserves user-edited scripts.
- Open an example in Editor or choose New script.
- Save with a simple filename, without
.luaor a path. Names allow Unicode letters, numbers, spaces, underscores and hyphens, up to 64 UTF-8 bytes. - Use Run Lua (or Ctrl+Enter) to execute the current editor buffer. While that script is running, the action becomes Unload; use it to stop the script, then Run again to apply edited code. Ctrl+S saves it. Run in the script manager runs the saved file, and becomes Unload for that running script.
- Use Script UI above the file list to select a script's custom page. Overlays render every frame. Autoload is opt-in per saved script.
The editor provides Lua syntax colors, line numbers, undo/redo and load-error markers. Unsaved buffers are retained while switching files in the current session. Save as refuses to overwrite a different existing file. Autoload choices are stored in autoload.txt; missing files are skipped. Stopping a script removes its pages, windows, owned features and timers. Changing a built-in setting is an explicit persistent setting change; stopping a script does not undo it.
The console retains globals across commands in its own @console Lua state. Enter a statement or prefix an expression with =. Up/Down recalls the last 100 commands. It can detach into a matching console window. Erroring commands retain the console state. The console counts toward the runtime's 16-state limit. Register pages, events and custom features in a script, not a console command.
=cs2.info().api_version
=entity.get_local_player()
for _,f in ipairs(settings.list('esp')) do print(f.id,f.type) end
Lua language-server metadata for the typed player/vector/settings interfaces is in /docs/cs2/cs2_v2.lua. Add it to an external editor's library paths; it is metadata, not a runnable script.
API coverage and porting
The exported binding index lists every inventoried host and shared global function with editor types. The reference comparison separates available features from engine APIs not yet exposed. cs2.capabilities() lets scripts inspect that distinction. Native hook methods, player/vector/color methods and callback payloads are documented below. Editor metadata is checked against the export registration and real Lua fixture output.
Console diagnostics
Errors identify the script, phase or callback, source line and Lua stack trace. The console timestamps entries, colors warnings/errors, supports text search and Errors only, and copies the currently visible messages with complete multiline traces. Auto-scroll follows new output only while you are already at the bottom.
A startup/compile failure stops the new script and rolls back its owned resources. A command worker failure discards its edits and disables that worker. HTTP transport errors identify the owning script and request phase; inspect the request result for details. A frame, UI, timer or event failure disables that callback and logs once; other healthy callbacks keep running. Reload to retry. Timer errors include their timer ID. Native-worker errors include the script, target address and traceback and are reported once on the next scripting frame; original behavior is retained. Inspect hook:status() for counters and the retained error.
| Message | What to check |
|---|---|
| compile / syntax error | Fix the named line; check missing end, quotes and parentheses. |
| attempt to index a nil value | Check optional players, bones and lookups before using them; map changes can invalidate data. |
| attempt to call a nil value | Verify spelling and availability. Use imgui.text, not ui.text. |
| bad argument / context | Check the signature, types, bounds and callback context in the API browser. |
| execution budget | Load/console: 200 ms. Regular callbacks share 8 ms per script per frame. Move setup out of frame callbacks and split repeated work. |
| not enough memory | Each script has a 16 MB Lua quota; reduce retained tables/strings. Native workers have 2 MB. |
| native hook disabled | Inspect its traceback, prototype and callback. Remove and recreate after fixing the source. |
console.warn, console.error and console.trace log developer messages without throwing or disabling the callback. console.trace adds the current Lua stack. Use Lua error(message) for an actual failure, or pcall when the script can recover. Execution budgets cannot interrupt a blocking C++/native call. The bounded console keeps the latest 512 messages; individual messages are capped at 8192 bytes, so extremely long traces can be truncated.
local function read_preferences()
local ok, value = pcall(storage.read, 'preferences', {})
if not ok then
console.warn('Preferences could not be read: ' .. tostring(value))
return {}
end
return value
end
console.trace('Preferences loaded')
CS2 visual templates
Framework - Player ESP draws its own boxes, names and health bars from copied players and projected positions. Its controls include a color picker and enemy filtering; it leaves built-in ESP settings alone. Framework - Asset downloads downloads binary data asynchronously, keeps the destination fixed during the request, and lists saved subfolders. Both are bundled and available from the website template index.
Standalone Menu is a floating tools window with F9 as its default open/close key. It includes a speed HUD, center crosshair and real CS2 ESP/box/name switches. ImGui Demo keeps the Simple-base UI-v2 panel layout and replaces its placeholder combat settings with custom crosshair sizing, speed HUD, local health ring, projected player labels, rainbow accent and native visual switches. F10 opens/closes its window. Both offer a key selector, Close window and explicit Save preferences. Hiding a window keeps its effects active; Unload removes its callbacks, windows and drawings. Native settings changed with settings.set keep their values.
The smaller Crosshair and Speed HUD scripts are starting templates for custom pages, toggle features, sampled player data and per-frame drawing. Templates skip missing/dead local players and unavailable projections. Distances and speed use Source units. Bundled visual templates never fetch or execute remote Lua. The Download asset template only downloads when the user supplies a URL and clicks its button. The Aimware v5 collection was reviewed as a visual-feature reference; its CSGO scripts are not drop-in CS2 scripts and were not copied into this package.
The manager's Other group contains only Open folder and Refresh. Edit, Run/Unload, Duplicate, Delete and Autoload remain in Script. Clipboard, save and undo tools remain in the editor; custom pages are reached through the browser's Script UI button.
Runtime contract
The runtime is Lua 5.4.7, reused from canonical simple-base/UI, with its Lua UI bindings compiled directly from canonical source and its text editor adapted to the native v2 shell. CS2_API_VERSION is 2.4; UI_API_VERSION is 1.1. It is not LuaJIT and is not binary/script compatible with other clients.
Each named script has separate globals, a 16 MB Lua allocator quota and a 1 MB source limit. At most 16 scripts run together. Loading and console evaluation have 200 ms instruction-hook budgets. Each running script has a shared 8 ms callback budget reset each frame, with at most 16 nested callbacks. These are instruction-hook limits, not real-time preemption of native C++ calls. Standard string, table, math, utf8, and base language functions are present. os, io, package, debug, dynamic load and LuaJIT FFI are not exposed. The separate native extension exposes integer addresses and typed Win64 calls/hooks.
Ordinary event/UI callbacks run on the rendering thread. Native hook callbacks run in isolated Lua workers on the calling native thread. Keep callbacks short. An error disables the failing callback; reload to re-enable it. A script can register 128 custom features and 32 callbacks per event; the UI layer allows 128 registrations across scripts. Timers allow 64 active entries per script and delays from 0.01 to 86,400 seconds. Missed repeating timer intervals coalesce into one callback; they do not replay a backlog. A timer can cancel itself or create another timer. Stop/reload/failed initialization releases owned resources.
events.on returns a token for events.off. Register events at load time. update runs after input and entity sampling; render permits drawing primitives. UI widgets belong inside ui.tab, ui.window, ui.overlay, or nested UI callbacks. The shared drawing layer allows 512 commands per callback (shared across render-event dispatch); additional CS2 draw helpers check a 250,000-vertex frame limit.
Native functions and hooks (2.2)
The native extension (version 1.0) supports loaded-module discovery, exports, wildcard code scans, guarded reads, relative/vtable resolution, typed function calls and synchronous detours. It uses the fixed Windows x64 ABI with at most 16 scalar arguments: bool, i32, u32, i64, u64, ptr, float, double; void is return-only. A member function takes its explicit object address as its first ptr. Lua integers carry addresses exactly. u64 uses the Lua integer's 64-bit bit pattern, so values above INT64_MAX appear negative. Struct/vector returns, varargs, vectorcall and C++ exceptions are not supported. Function addresses and prototypes must match the installed build; a wrong prototype, invalid object or native function fault can crash the process.
Scanning checks readable executable regions of a loaded module. Space-separated hex bytes and ?/?? wildcards are supported, up to 256 bytes; an all-wildcard pattern is rejected. With occurrence zero, missing and ambiguous signatures return nil,error. Positive occurrences are one-based. Scans have a 250 ms cap and are intended for script initialization, not every frame. Resolution/export/bind failures return nil,error; malformed arguments raise Lua errors. Relative offsets are bounded to 4096; vtable slots are zero-based 0..4095; reads are limited to 4096 bytes and never change page protection.
native.bind calls on the current thread; the author must satisfy the function's game-thread and object-lifetime requirements. Hook source must return function(original, ...). The source has its own globals, standard base/string/table/math/utf8 functions, native.read, read_bytes, relative, vtable, bind, and shared.get/set. It cannot capture the parent script's locals or access UI/events/settings. Exchange nil/boolean/integer/finite-number/string values using hook:get/set on the parent and shared.get/set in the worker. Up to 64 keys of 64 bytes and strings of 1024 bytes are stored per hook.
Native workers have a 2 MB allocator limit, 64 KB source limit, 50 ms initialization budget and a 2 ms/100,000-instruction callback budget. Budget checks cannot interrupt a blocking native call. Recursive entry or a busy worker immediately forwards to the original. An error disables that worker and forwards to the original; if it already called original, the existing result is preserved and native side effects are not repeated. original(...) may be called once per invocation. A healthy callback may replace arguments or return without calling original. Failed workers must be removed/recreated. Panic suspends native calls/callbacks at the next scripting frame.
Hooks are owned by the script even if the Lua handle is dropped. remove, unload, reload and failed script initialization retire the callback; a callback already in flight is allowed to finish. Other scripts cannot replace or remove its hook. Eight active hooks per script and 128 unique native targets per process are allowed. Retired targets may be reused only with the same prototype. Existing jump-entry detours are rejected instead of stacked. The private hook manager is independent of product hooks. Published pass-through bridges and trampolines remain bounded and resident, and their target/host modules are pinned until process exit so in-flight native calls cannot jump into freed code. Unloading scripts restores original behavior; after using native hooks, restarting the game is required to fully release that native bridge memory.
The bundled Native hooks template stays inactive until its pattern and exact prototype are filled in. It demonstrates a scalar-result modifier with a shared multiplier and call status. The ordinary CS2 entity/event API remains a copied/frame-sampled interface; this extension does not invent game signatures or expose a preverified create-move/trace/render ABI.
native.module
native.module(name) -> {base,size} or nil,error
local module,err=native.module("client.dll")
if module then print(module.base,module.size) end
native.export
native.export(module,name) -> address or nil,error
local address=assert(native.export("kernel32.dll","GetCurrentThreadId"))
native.scan
native.scan(module,pattern,occurrence=0) -> address or nil,error
-- Use an exact, verified signature for the installed game build.
local address,err=native.scan("client.dll",verified_pattern)
-- occurrence=0 requires exactly one match; 1 means first match.
native.relative
native.relative(address,displacement_offset=1,instruction_length=5) -> address or nil,error
-- E8 rel32: resolve the call target.
local target=assert(native.relative(call_address,1,5))
-- RIP-relative LEA/MOV commonly uses offsets 3,7; verify the instruction.
native.vtable
native.vtable(object_address,zero_based_index) -> address or nil,error
local target=assert(native.vtable(object_address,verified_slot))
native.read
native.read(address,type) -> scalar or nil,error
local pointer=assert(native.read(pointer_address,"ptr"))
local value=assert(native.read(pointer+verified_offset,"float"))
native.read_bytes
native.read_bytes(address,length) -> string or nil,error
local bytes=assert(native.read_bytes(verified_address,16))
native.bind
native.bind(address,signature) -> callable or nil,error
local address=assert(native.export("kernel32.dll","GetCurrentThreadId"))
local thread_id=assert(native.bind(address,{abi="win64",returns="u32",args={}}))
print(thread_id())
native.hook
native.hook(address,signature,callback_source) -> hook or nil,error
local hook=assert(native.hook(verified_address,
{abi="win64",returns="float",args={"ptr"}}, [[
return function(original,self)
return original(self)*(shared.get("multiplier") or 1)
end
]]))
hook:set("multiplier",1.1)
native.enable
hook:enable(boolean)
hook:enable(false) -- pass through
hook:enable(true) -- resume a healthy callback
native.remove
native.set
hook:set(key,scalar_or_nil)
hook:set("multiplier",1.25) -- worker reads shared.get("multiplier")
native.get
hook:get(key) -> scalar or nil
local calls=hook:get("samples") -- worker writes shared.set("samples",count)
native.status
hook:status() -> {enabled,removed,calls,bypassed,errors,error,address}
local status=hook:status()
print(status.calls,status.bypassed,status.error)
Entity data and events
entity.get_local_player(), entity.get(full_handle) and entity.get_players(enemies_only,alive_only) return copied player snapshots. These snapshots do not contain live memory addresses; native discovery is a separate explicit API. get takes the full pawn handle including its serial, not the player index. Snapshots retained in Lua stay unchanged; call p:refresh() for the current frame or p:is_valid() to check the handle still exists. A missing player, bone, weapon or screen projection returns nil. Positions, distances and velocities use Source world units; angles use degrees. UI coordinates and text sizes use framebuffer pixels; render.scale() reports the menu scale.
A player snapshot contains:
index,handle,name,sample_frame,health,armor,team,ping,flags,flash_duration.is_local,enemy,alive,scoped,crouched,defusing,helmet,defuser,immune.origin,eye_position,velocityas vectors;weaponas a copied table or nil.
A weapon snapshot contains handle, definition_index, ammo, zoom, and reloading. The bone API revalidates the controller's current full pawn handle and reads the existing animated-bone implementation; it never manufactures a pose. Bone indices range from 0 to 127. Player enumeration is limited to the existing 64 controller slots.
| Event | Arguments | Timing |
|---|---|---|
update |
none | Each rendered frame while scripts are running |
render |
none | Drawing phase, including the alternate stream-overlay path |
shutdown |
none | Stop or reload of a running script |
session_start, session_end |
none | Local player availability changes between samples |
player_health_changed |
current, previous player snapshots | Same full handle has different sampled health |
player_died, player_spawned |
current, previous snapshots | Same full handle changes sampled alive state |
local_weapon_changed |
current, previous snapshots | Local player's sampled weapon handle changes |
The CS2 state-change events are frame comparisons, not native server game events. Changes between samples can be missed. First appearance of a handle establishes a baseline. Respawn with a new handle also establishes a baseline; it does not infer a spawn event. There is no attacker, damage cause, round-event hook, create-move command editing, bullet trace or native render-material API in this version. Entity memory reads, bones, native projection and timing still require live-game validation on the current CS2 build.
Settings, controls and theme
settings.list(filter) exposes the actual CS2 registry IDs and metadata: ID, name, type, tab, group, range, choices and value. For example the master ESP ID is espEnabled. IDs are preserved from v2. settings.set checks the actual type and range, rejects nonfinite values, and marks the native configuration dirty. Dropdown setting values are zero-based; ui.combo uses Lua-style one-based indices. Color settings use normalized RGBA/RGB arrays.
features.get/set/list operate on boolean host settings and script-owned toggle/action features. features.add returns lua.<script name>.<local id>. features.bind and ui.keybind use the scripting runtime's binding state; features.active evaluates that state. Toggle bindings update the boolean setting; hold modes are queried by scripts through features.active. They do not rewrite existing product hotkey definitions. features.color is script feature metadata. Persist custom values explicitly with storage; they are recreated on reload.
Custom pages and subtabs are available through the Script UI selector. Tab section and icon metadata are accepted but do not add product-sidebar sections. Page overrides are limited to lua/scripts, lua/editor, lua/console, lua/ui, and lua/api. ui.feature_visible affects rows rendered through ui.feature or Custom features; it does not hide controls in built-in CS2 pages. ui.tr currently returns the supplied text. esp_colors.available() is false because v2 has its own native color registry; use settings for its actual color settings. Other inherited esp_colors operations return missing capability values or errors, rather than writing a disconnected color model.
Use render.theme() for accent, text, muted, panel and border colors. The native widget adapter follows the product palette. ui.theme/reset_theme and imgui.with_style customize standard ImGui controls/windows inside script rendering; they do not replace native product palette tokens. Script-created windows can attach to the left/right of the menu and optionally follow its visibility. Visible interactive Lua windows also receive the cursor when the main menu is closed, using the same Block Input policy as detached product windows. Closing or unloading the last script window releases that ownership; drawing-only overlays do not capture input.
Storage
storage reads/writes JSON values and files reads/writes binary-safe strings in a per-script directory: scripts/data/<UTF-8 script name encoded as hex>/. Keys are simple filenames; paths, device names and traversal are rejected. Writes replace the file atomically. Each write is limited to 64 KB. JSON conversion permits up to 16 nested levels and 16,384 visited values. Lua arrays must have integer keys; objects must have string keys. Plain empty tables encode as objects. json.array() and json.object() preserve explicit container types, including decoded empty arrays. JSON null decodes to Lua nil by default; pass true as the second argument to json.decode or third argument to storage.read to preserve it as json.null. Signed 64-bit integers round-trip exactly; JSON integers above INT64_MAX are rejected. Nonfinite numbers are rejected. Strings and object keys preserve embedded null bytes; JSON still requires valid UTF-8 strings. Cyclic tables and unsupported value types are errors. Renaming a script gives it a different data namespace.
Geometry and drawing additions (2.1)
vector2(x,y) provides 2D arithmetic and copy-returning geometry helpers. rect(x0,y0,x1,y1) takes top-left and bottom-right bounds; inverted bounds are rejected. Point containment includes edges; overlap/intersection require positive area, and disjoint rectangles return nil from intersect. Rectangle transforms return new objects.
colors.from_hex accepts RGB/RGBA short and long hex strings with an optional #. HSV hue uses degrees and wraps around 360; saturation, value, alpha and the existing color array components stay in 0..1. Helpers return independent values. These additions were informed by the Fatality Vec2, rectangle and color references; CS2 retains its own names and normalized color format.
render.screen_size() reports framebuffer dimensions. render.world_to_screen accepts either three coordinates or a vector, preserving its x,y-or-nil result. render.arc uses degrees, a 0..4096 radius, a signed span up to 360 degrees, and 3..256 segments. render.bezier draws a cubic through two endpoints and two control points, using 3..256 segments. Both accept thickness greater than 0 and up to 20, require a frame callback and respect the existing vertex limit. mathx.normalize_angle returns [-180,180); tick/time conversion uses the sampled game interval, rounds seconds to the nearest tick (half away from zero), and rejects unavailable intervals or counts beyond +/-1e9. The disconnected default interval is 1/64 second.
Reference design
The event/render/entity organization was informed by Neverlose events, Neverlose entities, Fatality script UI, and Fatality entities. The implementation uses the local Simple-base runtime and actual v2 host contracts. The upstream Simple-base snapshot is provenance, not the v2 compatibility contract.
Function reference
The following signatures and examples are generated from the same entries used by the in-app API browser. UI examples that only contain a control belong inside a UI callback. p, id, token, and timer_id denote previously obtained objects.
vector2
vector2(x=0,y=x) -> vector2
local p=vector2(30,40)
print(p:length(),p:normalized():unpack())
rect
rect(x0=0,y0=0,x1=0,y1=0) -> rectangle
local panel=rect(20,20,220,100)
print(panel:width(),panel:height(),panel:center():unpack())
colors.from_hex
colors.from_hex('#RGB|RGBA|RRGGBB|RRGGBBAA') -> color (0..1)
local accent=colors.from_hex('#479cff')
print(accent:alpha(.5):to_hex())
colors.from_hsv
colors.from_hsv(hue_degrees,saturation,value,alpha=1) -> color
local accent=colors.from_hsv(210,.8,1,.9)
colors.to_hsv
colors.to_hsv(rgba) -> hue_degrees,saturation,value,alpha
local h,s,v,a=colors.to_hsv(render.theme().accent)
local copy=colors.from_hsv(h,s,v,a)
vector2.clone
vector2.unpack
vector2.length
vector2.length_sqr
vector2.normalized
vector2.dot
vector2.distance
vector2.lerp
vector2.floor
vector2.ceil
vector2.round
rect.clone
rect.width
rect.height
rect.size
rect.center
rect.contains
r:contains(point_or_rect) -> boolean
local r=rect(20,20,220,100)
local result=r:contains(vector2(30,40))
rect.overlaps
r:overlaps(other) -> boolean
local r=rect(20,20,220,100)
local result=r:overlaps(rect(0,0,100,100))
rect.intersect
r:intersect(other) -> rectangle or nil (no positive-area intersection)
local r=rect(20,20,220,100)
local result=r:intersect(rect(0,0,100,100))
rect.translate
r:translate(delta) -> rectangle
local r=rect(20,20,220,100)
local result=r:translate(vector2(10,20))
rect.expand
rect.shrink
color.clone
color.to_hsv
c:to_hsv() -> hue_degrees,saturation,value,alpha
local c=color(.2,.6,1)
local result=c:to_hsv()
render.screen_size
render.arc
render.arc(x,y,radius,start_degrees,end_degrees,color,thickness=1,segments=48)
events.on('render',function()
render.arc(90,90,30,-90,180,color(.2,.7,1),3)
end)
render.bezier
render.bezier(x1,y1,x2,y2,x3,y3,x4,y4,color,thickness=1,segments=32)
events.on('render',function()
render.bezier(20,90,60,10,120,10,160,90,color(1,.5,.2),2)
end)
mathx.normalize_angle
mathx.seconds_to_ticks
mathx.seconds_to_ticks(seconds) -> nearest integer tick count
local ticks=mathx.seconds_to_ticks(.5)
mathx.ticks_to_seconds
mathx.ticks_to_seconds(integer_ticks) -> seconds
local seconds=mathx.ticks_to_seconds(32)
cs2.info
cs2.connected
cs2.map_name
cs2.clock
cs2.view_angles
entity.get_local_player
entity.get_players
entity.get_players(enemies_only=false, alive_only=false) -> players
for _,p in ipairs(entity.get_players(true,true)) do print(p.name,p.health) end
entity.get
entity.bone
entity.bone(full_handle, bone_index) -> vector or nil
local head = entity.bone(player.handle,6)
entity.weapon
entity.weapon(full_handle) -> weapon snapshot or nil
local gun = entity.weapon(player.handle)
settings.get
settings.set
settings.list
settings.list(filter='') -> metadata[]
for _,f in ipairs(settings.list('esp')) do print(f.id,f.type) end
settings.info
settings.info(feature_id) -> metadata or nil
print(json.encode(settings.info('espEnabled')))
input.is_key_down
input.is_key_pressed
input.is_key_pressed(vk_code) -> boolean
if input.is_key_pressed(0x56) then print('V') end
input.mouse_position
timers.after
timers.every
timers.every(seconds, callback) -> id
local id=timers.every(1,function() print(cs2.clock().tick) end)
timers.cancel
storage.read
storage.read(key, fallback=nil, preserve_null=false) -> value
local prefs=storage.read('preferences',{enabled=true})
storage.write
storage.delete
files.read
files.write
files.list
json.encode
json.decode
json.decode(text, preserve_null=false) -> value
local value=json.decode('{"health":100}')
render.world_to_screen
render.world_to_screen(x,y,z) or render.world_to_screen(vector) -> x,y or nil
local x,y=render.world_to_screen(p.origin.x,p.origin.y,p.origin.z)
render.measure_text
render.measure_text(text,size=14) -> width,height
local w,h=render.measure_text('CS2',14)
render.gradient
render.gradient(x,y,w,h,rgba_top,rgba_bottom)
render.gradient(20,20,160,40,{.2,.6,.8,1},{.1,.1,.1,1})
render.triangle
render.triangle(x1,y1,x2,y2,x3,y3,color,filled=true)
render.triangle(20,20,40,20,30,40,{1,1,1,1})
render.polyline
render.polyline({{x,y},...},color,thickness=1,closed=false)
render.polyline({{10,10},{30,30},{50,10}},{1,1,1,1})
render.theme
render.scale
console.log
console.clear
script.name
mathx.distance
mathx.calc_angle
mathx.calc_angle(x1,y1,z1,x2,y2,z2) -> vector
local angles=mathx.calc_angle(0,0,0,100,100,0)
mathx.angle_fov
mathx.angle_fov(pitch,yaw,target_pitch,target_yaw) -> number
local fov=mathx.angle_fov(0,0,10,20)
mathx.clamp
mathx.lerp
mathx.remap
mathx.remap(value,in_min,in_max,out_min,out_max) -> number
local width=mathx.remap(player.health,0,100,0,160)
features.add
features.add({id,label,category,description,default,kind,key,on_trigger}) -> feature_id
local id=features.add{id='my_feature',label='My feature',default=true}
ui.tab('controls','Controls',function() ui.feature(id); ui.keybind(id) end)
features.get
features.set
features.active
features.trigger
features.trigger(id)
local action=features.add{id='hello',kind='action',on_trigger=function()print('hello')end}
features.trigger(action)
features.list
features.list() -> entries[]
for _,f in ipairs(features.list()) do print(f.id,f.label) end
features.bind
features.color
features.color(id, r, g, b, a)
local id=features.add{id='my_feature'}
features.color(id,.3,.6,.8,1)
base.get
base.set
base.log
events.on
events.on(event_name, callback) -> token
local token
token=events.on('update',function() print(cs2.clock().tick); events.off(token) end)
events.off
events.names
update | render | shutdown | session_start | session_end | player_health_changed | player_died | player_spawned | local_weapon_changed
events.on('player_health_changed',function(now,previous) print(now.name,previous.health,now.health) end)
ui.tab
ui.tab(id,label,callback,options={}) -> page_id
ui.tab('my_page','My page',function() imgui.text('Hello') end)
ui.subtab
ui.subtab(parent_page_id,id,label,callback) -> page_id
local page=ui.tab('tools','Tools')
ui.subtab(page,'status','Status',function()imgui.text('Ready')end)
ui.overlay
ui.overlay(id,callback) -> id
ui.overlay('label',function()render.text(20,20,'Hello',render.theme().text)end)
ui.window
ui.window(id,title,{width,height,attach,menu_only},callback) -> id
ui.window('status','Status',{width=320,height=200,attach='right',menu_only=true},function()imgui.text('Hello')end)
ui.override
ui.override("lua/scripts" | "lua/editor" | "lua/console" | "lua/ui" | "lua/api",callback)
ui.override('lua/console',function()imgui.text('Custom console page')end)
ui.theme
ui.theme({rounding,alpha,colors={Text=rgba,...}})
ui.theme{rounding=5,colors={Text={.9,.95,1,1}}}
ui.reset_theme
ui.tr
ui.feature
ui.feature(feature_id)
ui.tab('controls','Controls',function()ui.feature('espEnabled')end)
ui.keybind
ui.keybind(feature_id,label="Key")
ui.tab('keys','Keys',function()ui.keybind('espEnabled','ESP key')end)
ui.feature_visible
ui.window_visible
ui.window_visible(window_id,visible)
local id=ui.window('x','X',{},function()imgui.text('X')end)
ui.window_visible(id,false)
ui.entity_colors
ui.group
ui.columns
ui.columns(count,callback,stagger=false)
ui.columns(2,function()imgui.text('Left');ui.next_column();imgui.text('Right')end)
ui.next_column
ui.slider
ui.slider(label,value,min,max) -> changed,value
local changed,value=ui.slider('Scale',1,.5,2)
ui.slider_int
ui.slider_int(label,value,min,max) -> changed,value
local changed,value=ui.slider_int('Count',5,1,20)
ui.toggle
ui.button
ui.button(label,width=0,height=0) -> clicked
if ui.button('Print') then print('clicked') end
ui.selectable
ui.selectable(label,selected,width=0,height=0) -> clicked
if ui.selectable('Item',false) then print('selected') end
ui.combo
ui.combo(label,index,labels) -> changed,index (1-based)
local changed,index=ui.combo('Mode',1,{'A','B'})
ui.multi_combo
ui.multi_combo(label,selected[],labels[]) -> changed,selected[]
local changed,selected=ui.multi_combo('Flags',{true,false},{'A','B'})
imgui.text
imgui.text_colored
imgui.button
imgui.checkbox
imgui.checkbox(label,value) -> changed,value
local changed,enabled=imgui.checkbox('Enabled',true)
imgui.slider_float
imgui.slider_float(label,value,min,max) -> changed,value
local changed,v=imgui.slider_float('Value',1,0,10)
imgui.slider_int
imgui.slider_int(label,value,min,max) -> changed,value
local changed,v=imgui.slider_int('Count',1,0,10)
imgui.input_text
imgui.input_text(label,text) -> changed,text
local changed,text=imgui.input_text('Name','example')
imgui.combo
imgui.combo(label,index,labels) -> changed,index
local changed,index=imgui.combo('Mode',1,{'A','B'})
imgui.color_edit
imgui.color_edit(label,rgba) -> changed,rgba
local changed,c=imgui.color_edit('Color',{1,1,1,1})
imgui.same_line
imgui.separator
imgui.spacing
imgui.tooltip
imgui.progress
imgui.child
imgui.child(id,width,height,callback)
imgui.child('content',0,160,function()imgui.text('Child')end)
imgui.window
imgui.window(title,{width,height},callback)
imgui.window('Tools',{width=320,height=200},function()imgui.text('Hello')end)
imgui.disabled
imgui.disabled(disabled,callback)
imgui.disabled(true,function()ui.button('Disabled')end)
imgui.with_style
imgui.with_style(colors,callback)
imgui.with_style({Text={1,.7,.3,1}},function()imgui.text('Amber')end)
imgui.available
imgui.cursor
imgui.set_cursor
imgui.is_item_hovered
render.text
render.line
render.rect
render.rect(x,y,width,height,rgba,filled=false,rounding=0)
render.rect(20,20,100,50,{.1,.2,.3,1},true,5)
render.circle
render.circle(x,y,radius,rgba,filled=false,thickness=1)
render.circle(100,100,20,{1,1,1,1})
engine.time
engine.delta_time
engine.fps
engine.viewport
vector
vector(x=0,y=0,z=0) -> vector
local position=vector(3,4,0)
print(position:length(),(position*2):unpack())
vector:clone
vector:unpack
vector:length
vector:length_sqr
vector:length2d
vector:normalized
vector:dot
vector:cross
vector:distance
vector:lerp
color
color:unpack
color:alpha
color:lerp
color:to_hex
player:get_name
player:get_origin
player:get_eye_position
p:get_eye_position()
local p=entity.get_local_player()
if p then print(p:get_eye_position()) end
player:get_velocity
p:get_velocity()
local p=entity.get_local_player()
if p then print(p:get_velocity()) end
player:get_health
player:get_armor
player:is_alive
player:is_enemy
player:get_weapon
player:get_bone
player:is_valid
player:refresh
esp_colors.available
Utilities, animation and lossless JSON (2.3)
Binary utilities accept at most 64 KB of decoded data. Base64 uses the standard alphabet and requires canonical padding without whitespace. Hex accepts upper/lowercase pairs. utils.from_bytes requires a dense array of bytes 0..255; embedded zero bytes survive all conversions. FNV-1a is a noncryptographic 32-bit hash. files.exists and files.delete use the same script-specific directory and filename rules as reads/writes.
Animation helpers use degrees for angles and seconds for damping. mathx.damp uses exponential interpolation so the same elapsed time gives the same result at different frame rates for a fixed target. Approach functions clamp the step; angular helpers take the shortest arc. mathx.ease supports linear, in/out/in_out quad, cubic and sine curves. Smoothstep/easing clamp progress to 0..1. Math inputs must be finite and within +/-1e9.
local data = json.decode('[9223372036854775807,null,[]]', true)
assert(data[2] == json.null)
assert(json.encode(data) == '[9223372036854775807,null,[]]')
local alpha = 0
ui.overlay('smooth_status', function()
alpha = mathx.damp(alpha, 1, 8)
render.text(24, 100, 'Ready', color(1,1,1,alpha), 16)
end)
cs2.capabilities
utils.base64_encode
utils.base64_decode
utils.hex_encode
utils.hex_decode
utils.to_bytes
utils.from_bytes
utils.fnv1a
utils.unix_time
mathx.approach
mathx.approach_angle
mathx.approach_angle(current,target,max_degrees) -> [-180,180)
print(mathx.approach_angle(170,-170,5))
mathx.lerp_angle
mathx.remap_clamped
mathx.remap_clamped(value,in_min,in_max,out_min,out_max) -> number
print(mathx.remap_clamped(150,0,100,0,1))
mathx.smoothstep
mathx.smootherstep
mathx.damp
mathx.damp(current,target,rate,dt=engine.delta_time()) -> number
local next_value=mathx.damp(0,1,8,1/60)
mathx.ease
mathx.angle_vectors
mathx.angle_vectors(pitch,yaw,roll=0) -> forward,right,up
local forward,right,up=mathx.angle_vectors(0,90)
mathx.vector_angles
mathx.vector_angles(direction) -> angles or nil
local angles=mathx.vector_angles(vector(1,1,0))
files.exists
files.delete
json.array
json.object
console.warn
console.error
console.trace
json.null
Opaque non-nil sentinel for an explicit JSON null. Use json.decode(text, true) or storage.read(key, fallback, true) to retain it. It is not a general-purpose serializable userdata.
Script permissions
Open Scripts > Script permissions to change host-owned toggles. cs2.permissions() returns files_read, files_write, http, modules, commands, native, and inventory. Scripts cannot grant themselves permissions. File read/write default on for compatibility with existing scripts; HTTP, module execution, custom commands, native access and inventory changes default off. The choices persist in C:\scooby\CS2-v2\lua-permissions.json, outside script storage. Legacy files and storage obey the same read/write switches.
Turning HTTP off cancels outstanding requests and blocks their results. Turning commands/native access off stops their effects; healthy callbacks can resume when re-enabled. Already executed Lua modules remain part of their script until unload. Native access is powerful and can call external functions; these service toggles are controls on the managed APIs, not a security boundary against a script that has native permission.
cs2.capabilities() reports compiled API support. Check permissions separately, commands.engine_status() for input/trace readiness, and model_preview.status() / inventory.status() for native availability. A compiled API does not imply that every current game build or scene can use its engine service.
Custom command frameworks
commands.create(source,{enabled=true}) registers one isolated command worker per script during script loading. Its source must return function(cmd,ctx). Up to eight workers run in registration order. They run on CS2's command thread, after enabled host movement processing, using the existing validated command serializer and rollback. Active command workers suppress the built-in aimbot/RCS and triggerbot so authors can own target selection, recoil correction and firing. If you only want an overlay, use ordinary ui.overlay / events.on('render',...) callbacks.
Commands run only with a live local player, foreground game, closed menu and the panic switch off. An unsupported command pipeline does not run callbacks; inspect commands.engine_status().message. The engine adapter and live trace/model/inventory behavior require in-game acceptance for the installed CS2 build; offline fixtures validate worker execution and host contracts.
| Command field | Contract |
|---|---|
number |
Read-only command sequence |
pitch, yaw |
Finite degrees: -89..89 and -180..180 |
forward, left, up |
Normalized movement: -1..1 |
buttons |
Unsigned 32-bit mask; attack=1, jump=2, duck=4; native higher bits are preserved |
visible |
Boolean, defaults true; update the visible camera only after successful serialization |
ctx contains tick, time, interval (the host command model uses 1/64 second), local_player, players, recoil, velocity and weapon. Players are copied {index,handle,health,armor,team,enemy,alive,immune,origin,eye} records with {x,y,z} vectors; players excludes self. weapon has handle, definition, ammo, reloading, can_fire. can_fire is a readiness check, not a hit guarantee. Recoil values are the host aim-punch angles; authors choose compensation and smoothing.
Worker-only game.trace(start,finish) uses the host shot trace, skips the local pawn and returns {fraction,start_solid,entity,position,normal} or nil,error. It is not penetration simulation. game.bone(handle,index) reads an actual animated player bone (0..127), returning nil when unavailable; it does not fabricate a fallback pose. Handles are revalidated against the current command's player identities. game.key_down(vk) checks virtual keys 1..255. These functions are deliberately separate from render-thread player snapshots.
Use callback:set(key,value) and worker shared.get(key) for UI-to-command controls; worker shared.set and callback:get carry results back. Values are scalars, nil or strings up to 4096 bytes, with 64 keys per worker. Worker state is separate from the UI Lua state: no UI, files, HTTP, native calls, dynamic code loading or protected calls. Base/table/string/math/utf8 are available. Limits are 4 MiB, 200,000 instructions, 2 ms per callback, 4 ms for the dispatch and 64 game queries. A blocking native query cannot be interrupted mid-call, but an over-budget result is discarded. Rebuilt identical commands reuse the prior result without advancing worker state twice. Altered input for an already processed sequence is left untouched.
callback:enable, callback:remove, callback:status, callback:get and callback:set control the worker. Errors discard that worker's edits, disable it, and report the script, worker source/line, traceback when available and a reload hint to the console. Other callbacks remain active. Unload, reload and failed startup remove owned workers. The Command framework template starts disabled and demonstrates custom targeting, recoil and firing with explicit controls.
Folder storage and module loading
fs adds nested paths alongside the legacy simple-name files/storage APIs. Each script has its own stable folder under C:\scooby\CS2-v2\lua-data; renaming a script selects a different namespace. A path is relative to that folder, maximum 500 UTF-8 bytes. Absolute paths, traversal, device names, alternate streams, links and junctions are rejected. The directory checks do not isolate a script with native access or an external process changing the filesystem concurrently.
Read/write at most 8 MiB per file. fs.write creates parents and atomically replaces a file. fs.list('',true) recursively lists up to 2048 sorted {path,directory,size} entries. fs.rename refuses to overwrite a destination. fs.remove removes a file or an empty directory; it does not recursively erase trees. Invalid argument types or disabled permissions raise an error; operational failures return nil,error, so use assert or handle the returned message.
fs.load(path) requires read and module permissions. It executes text-only Lua of at most 1 MiB in the calling script state and returns the module's first result or nil,error with source information. It does not cache modules. If the module registers UI or callbacks, load it during script startup. Downloading a file never executes it automatically. json.encode/decode work with these files for profiles and save/load workflows.
HTTP requests and download workflows
http.request{url,method='GET',headers={},body='',max_bytes=8388608} starts an asynchronous HTTP/HTTPS request and returns an owned request handle or nil,error. Methods: GET, HEAD, POST, PUT, PATCH, DELETE. Request bodies are at most 1 MiB; responses at most 8 MiB; headers at most 16 KiB. A script may retain eight request handles, with four running requests globally. Release completed handles to create more. Network I/O stays off the UI/command threads.
Poll request:status() for pending, complete or canceled. request:result() returns {status,body,ok,url} or nil,error (pending while unfinished). HTTP 4xx/5xx are completed responses with ok=false, not transport errors. body is binary-safe; pass it to fs.write to save assets or modules. Redirects are returned as 3xx, not followed automatically. Embedded URL credentials and newline injection are rejected. TLS certificate validation remains enabled; automatic cookies and Windows authentication are disabled. The transport has 3-second operation timeouts and a 20-second response-body deadline, not a guaranteed total wall-time limit.
request:cancel(), script unload or disabling HTTP marks requests canceled. A worker may take until its current network operation returns to release transport resources. No callback is invoked after script teardown. The Download asset template shows polling and save/load; choose your own update URL and explicitly decide whether and when to execute downloaded Lua. HTTP is not an HTML/CSS browser renderer. WebSocket and embedded web UI remain unavailable.
Local inventory and native 3D previews
The inventory API operates the host's local inventory changer. It does not create Steam-owned/tradable items or modify an account's server inventory. IDs are decimal strings to preserve all 64 bits. Catalog and owned-item reads return copies, paged from 1 with a maximum of 256 entries per call. inventory.categories() lists supported catalog families; inventory.catalog returns {category,definition,auxiliary,name,rarity,resource}. inventory.finishes returns current {paint_kit,name,rarity,old_model} entries for weapons, knives or gloves; all=true includes paint kits authored for other models.
Draft identity is {category,definition,auxiliary=0} and must exist in the host catalog. Optional cosmetics: paint_kit, wear (0..1), seed (0..1000), stattrak (-1..999999), custom_name (160 bytes), stickers and sticker_wear arrays with exactly inventory.status().sticker_slots entries (five in the current build), charm, charm_seed, and three charm_offset values (-10..10). Paint kits must exist in the current host catalog. Unknown keys and changing identity through inventory.update are errors. The host owns resource paths and rarity. Add is limited to 4096 locally owned items.
inventory.add/update/remove/equip/unequip require inventory permission. Successful changes persist; failed saves roll back the local transaction. Team masks are CT=1, T=2, both=3; equipping clears the same slot on the requested teams and preserves the other team's selection. true means the local operation was accepted; native application may still be waiting for an in-game session or supported schema. Read inventory.status() for the local store, native bridge, apply message and revision.
model_preview.draw(label,id_or_draft,width,height,transparent=false) belongs inside a UI drawing callback; dimensions are 32..2048. It returns {visible,ready,x1,y1,x2,y2} or nil,error and uses the existing host 3D renderer and its drag/zoom controls. The preview service supports one live model at a time. A validated draft can be previewed without adding or equipping an item. The preview obeys the native live-preview setting and renderer availability. Unload closes a preview owned by that script. Arbitrary imported meshes, managed textures, shaders and font resources are not added by this API.
Framework function signatures
cs2.permissions
fs.read
fs.write
fs.write(path,bytes) -> true or nil,error
assert(fs.write('profiles/default.json',json.encode({enabled=true})))
fs.mkdir
fs.exists
fs.list
fs.list(path,recursive=false) -> entries or nil,error
for _,entry in ipairs(assert(fs.list('',true))) do print(entry.path,entry.directory,entry.size) end
fs.rename
fs.rename(from,to) -> true or nil,error
assert(fs.rename('profiles/draft.json','profiles/current.json'))
fs.remove
fs.load
fs.load(path) -> module_value or nil,error
local module=assert(fs.load('modules/helpers.lua'))
http.request
http.request(options) -> HttpRequest or nil,error
local request=assert(http.request{url='https://example.com/version.json',max_bytes=65536})
commands.available
commands.engine_status
commands.engine_status() -> {ready,trace,message}
print(commands.engine_status().message)
commands.create
commands.create(source,options?) -> CommandCallback or nil,error
local callback=assert(commands.create([[return function(cmd,ctx)
if game.key_down(1) then cmd.pitch=math.max(-89,math.min(89,cmd.pitch-ctx.recoil.x*2)) end
end]],{enabled=false}))
inventory.categories
inventory.categories() -> string[]
for _,category in ipairs(inventory.categories()) do print(category) end
inventory.catalog
inventory.catalog(category,start=1,limit=100) -> entries,total
local entries,total=inventory.catalog('weapon',1,100)
inventory.finishes
inventory.finishes(category,definition,start=1,limit=100,all=false) -> entries,total
local finishes,total=inventory.finishes('weapon',7,1,100)
inventory.items
inventory.items(start=1,limit=100) -> items,total
local owned,total=inventory.items(1,100)
inventory.get
inventory.add
inventory.add(draft) -> decimal_id or nil,error
local id=assert(inventory.add{category='weapon',definition=7,wear=.1,seed=1})
inventory.update
inventory.update(id,cosmetics) -> true or nil,error
assert(inventory.update(saved_id,{wear=.2,custom_name='My item'}))
inventory.remove
inventory.equip
inventory.equip(id,teams=3) -> true or nil,error
assert(inventory.equip(saved_id,1)) -- CT=1, T=2, both=3
inventory.unequip
inventory.unequip(id,teams=3) -> true or nil,error
assert(inventory.unequip(saved_id,3))
inventory.status
inventory.status() -> {local,native,apply,revision,sticker_slots}
local status=inventory.status();print(status['local'],status.native,status.apply)
model_preview.draw
model_preview.draw(label,id_or_draft,width,height,transparent=false) -> {visible,ready,x1,y1,x2,y2} or nil,error
ui.tab('models','Models',function()
model_preview.draw('AK preview',{category='weapon',definition=7},320,280)
end)
model_preview.status
HttpRequest.status
HttpRequest.result
request:result() -> {status,body,ok,url} or nil,error
local response,err=request:result()
if response then print(response.status,#response.body) end
HttpRequest.cancel
CommandCallback.enable
CommandCallback.remove
CommandCallback.get
CommandCallback.set
CommandCallback.status
callback:status() -> {enabled,removed,failed,calls,replays,error}
print(callback:status().error)
CommandGame.trace
game.trace(start,finish) -> {fraction,start_solid,entity,position,normal} or nil,error
local hit,err=game.trace(ctx.local_player.eye,ctx.players[1].eye)
CommandGame.bone
game.bone(handle,index) -> {x,y,z} or nil,error
local head=game.bone(ctx.players[1].handle,6)
CommandGame.key_down
game.key_down(virtual_key) -> boolean
if game.key_down(0x05) then cmd.buttons=cmd.buttons|1 end
Binding index
Download MarkdownCS2 exported binding index
Generated from host/shared registration and checked editor signatures. UI widgets require a UI callback; render primitives require a frame callback. Host-specific behavior and limits are in the host contract. widgets and ui.widgets refer to the same table. Standard Lua libraries are documented by Lua 5.4; native hook handles and value-object methods are covered in the host contract and editor definitions.
base.get
base.log
base.set
color
colors.from_hex
colors.from_hsv
colors.from_hsv(hue_degrees,saturation,value,alpha)
hue_degrees number; saturation number; value number; alpha? number; Returns: CS2Color
colors.to_hsv
commands.available
commands.available() -> boolean
Returns: boolean
print(commands.available()) -- permission state
commands.create
commands.create(source,options?) -> CommandCallback or nil,error
source string; options CommandOptions?; Returns: CommandCallback?; Returns: string? error
local callback=assert(commands.create([[return function(cmd,ctx)
if game.key_down(1) then cmd.pitch=math.max(-89,math.min(89,cmd.pitch-ctx.recoil.x*2)) end
end]],{enabled=false}))
commands.engine_status
commands.engine_status() -> {ready,trace,message}
Returns: table
print(commands.engine_status().message)
console.clear
console.error
console.error(message)
message string
Log only; use error(message) to raise an actual Lua failure.
console.error("Could not load custom preferences")
console.log
console.log(text)
message string
Log only; use error(message) to raise an actual Lua failure.
console.log('Script loaded')
console.trace
console.trace(message)
message string
Log only; use error(message) to raise an actual Lua failure.
console.trace("Reached update handler")
console.warn
console.warn(message)
message string
Log only; use error(message) to raise an actual Lua failure.
console.warn("Optional data is unavailable")
cs2.capabilities
cs2.capabilities() -> table
Returns: table<string,boolean>
Explicit capabilities; false entries are not implemented by this host.
print(json.encode(cs2.capabilities()))
cs2.clock
cs2.clock() -> {curtime,tick,interval,realtime,frame}
Returns: {curtime:number,tick:integer,interval:number,realtime:number,frame:integer}
local tick = cs2.clock().tick
cs2.connected
cs2.connected() -> boolean
Returns: boolean
if cs2.connected() then print(cs2.map_name()) end
cs2.info
cs2.map_name
cs2.permissions
cs2.view_angles
draw.circle
draw.circle(x,y,radius,rgba,filled,thickness)
x number; y number; radius number; rgba CS2Color|number[]; filled? boolean; thickness? number
draw.line
draw.line(x0,y0,x1,y1,rgba,thickness)
x0 number; y0 number; x1 number; y1 number; rgba CS2Color|number[]; thickness? number
draw.rect
draw.rect(x,y,width,height,rgba,filled,rounding)
x number; y number; width number; height number; rgba CS2Color|number[]; filled? boolean; rounding? number
draw.text
draw.text(x,y,text,rgba,size)
x number; y number; text string; rgba CS2Color|number[]; size? number
draw.triangle
draw.triangle(x0,y0,x1,y1,x2,y2,rgba,filled,thickness)
x0 number; y0 number; x1 number; y1 number; x2 number; y2 number; rgba CS2Color|number[]; filled? boolean; thickness? number
engine.delta_time
engine.fps
engine.time
engine.viewport
entity.bone
entity.bone(full_handle, bone_index) -> vector or nil
full_handle integer; bone_index integer; Returns: CS2Vector?
local head = entity.bone(player.handle,6)
entity.get
entity.get(full_handle) -> player or nil
full_handle integer; Returns: CS2Player?
local current = entity.get(saved_handle)
entity.get_local_player
entity.get_local_player() -> player or nil
Returns: CS2Player?
local me = entity.get_local_player()
entity.get_players
entity.get_players(enemies_only=false, alive_only=false) -> players
enemies_only? boolean; alive_only? boolean; Returns: CS2Player[]
for _,p in ipairs(entity.get_players(true,true)) do print(p.name,p.health) end
entity.weapon
entity.weapon(full_handle) -> weapon snapshot or nil
full_handle integer; Returns: CS2Weapon?
local gun = entity.weapon(player.handle)
esp_colors.available
esp_colors.available()
Returns: boolean
Availability probe. CS2 uses settings.get/set/list for native ESP colors.
Returns false in CS2.
esp_colors.categories
esp_colors.categories(...)
...: any; Returns: any
Unavailable. CS2 uses settings.get/set/list for native ESP colors.
Unavailable in CS2; use settings.get/set/list for native ESP colors.
esp_colors.enabled
esp_colors.enabled(...)
...: any; Returns: any
Unavailable. CS2 uses settings.get/set/list for native ESP colors.
Unavailable in CS2; use settings.get/set/list for native ESP colors.
esp_colors.get
esp_colors.get(...)
...: any; Returns: any
Unavailable. CS2 uses settings.get/set/list for native ESP colors.
Unavailable in CS2; use settings.get/set/list for native ESP colors.
esp_colors.reset
esp_colors.reset(...)
...: any; Returns: any
Unavailable. CS2 uses settings.get/set/list for native ESP colors.
Unavailable in CS2; use settings.get/set/list for native ESP colors.
esp_colors.set
esp_colors.set(...)
...: any; Returns: any
Unavailable. CS2 uses settings.get/set/list for native ESP colors.
Unavailable in CS2; use settings.get/set/list for native ESP colors.
events.off
events.on
events.on(event,callback)
event CS2Event; callback fun(current?:CS2Player,previous?:CS2Player); Returns: integer token
features.active
features.add
features.add(options)
options {id:string,label:string,description?:string,category?:string,kind?:string,default?:boolean,key?:string,mode?:string,callback?:function}; Returns: string
features.bind
features.color
features.get
features.list
features.set
features.trigger
files.delete
files.delete(name) -> boolean,error?
name string; Returns: boolean, string?
local removed,err=files.delete("old notes")
files.exists
files.exists(name) -> boolean
name string; Returns: boolean
print(files.exists("notes"))
files.list
files.list() -> names[]
Returns: string[]
for _,name in ipairs(files.list()) do print(name) end
files.read
files.read(name) -> string or nil
name string; Returns: string?
local text=files.read('notes')
files.write
files.write(name, text)
name string; bytes string
Binary-safe, at most 65536 bytes. Simple names only; scoped to this script.
files.write('notes','hello')
fs.exists
fs.exists(path) -> boolean or nil,error
path string; Returns: boolean?; Returns: string? error
local exists,err=fs.exists('assets/icons')
fs.list
fs.list(path,recursive=false) -> entries or nil,error
path string; recursive boolean?; Returns: ScriptFileEntry[]?; Returns: string? error
for _,entry in ipairs(assert(fs.list('',true))) do print(entry.path,entry.directory,entry.size) end
fs.load
fs.load(path) -> module_value or nil,error
path string; Returns: any; Returns: string? error
local module=assert(fs.load('modules/helpers.lua'))
fs.mkdir
fs.mkdir(path) -> true or nil,error
path string; Returns: boolean?; Returns: string? error
assert(fs.mkdir('assets/icons'))
fs.read
fs.read(path) -> string or nil,error
path string; Returns: string?; Returns: string? error
local data,err=fs.read('profiles/default.json')
fs.remove
fs.remove(path) -> boolean or nil,error
path string; Returns: boolean?; Returns: string? error
assert(fs.remove('profiles/unused.json'))
fs.rename
fs.rename(from,to) -> true or nil,error
from string; to string; Returns: boolean?; Returns: string? error
assert(fs.rename('profiles/draft.json','profiles/current.json'))
fs.write
fs.write(path,bytes) -> true or nil,error
path string; bytes string; Returns: boolean?; Returns: string? error
assert(fs.write('profiles/default.json',json.encode({enabled=true})))
http.request
http.request(options) -> HttpRequest or nil,error
options HttpOptions; Returns: HttpRequest?; Returns: string? error
local request=assert(http.request{url='https://example.com/version.json',max_bytes=65536})
imgui.available
imgui.button
imgui.button(label,width,height)
label string; width? number; height? number; Returns: boolean
imgui.checkbox
imgui.child
imgui.child(id,width,height,draw,options)
id string; width number; height number; draw function; options? {border?:boolean,padding?:boolean,horizontal_scroll?:boolean}
imgui.close_popup
imgui.close_popup()
imgui.color_edit
imgui.color_edit(label,rgba)
label string; rgba CS2Color|number[]; Returns: boolean, CS2Color
imgui.combo
imgui.combo(label,index,items)
label string; index integer; items string[]; Returns: boolean, integer
One-based index; 1..128 items.
imgui.combo_custom
imgui.combo_custom(label,preview,draw)
label string; preview string; draw function; Returns: boolean
imgui.cursor
imgui.cursor_local
imgui.disabled
imgui.drag_float
imgui.drag_float(label,value,min,max,speed)
label string; value number; min number; max number; speed? number; Returns: boolean, number
imgui.drag_int
imgui.drag_int(label,value,min,max,speed)
label string; value number; min number; max number; speed? number; Returns: boolean, number
imgui.dummy
imgui.group
imgui.input_text
imgui.input_text(label,text)
label string; text string; Returns: boolean, string
Maximum 4095 UTF-8 bytes.
imgui.invisible_button
imgui.invisible_button(id,width,height)
id string; width number; height number; Returns: boolean
imgui.is_item_active
imgui.is_item_clicked
imgui.is_item_clicked(button)
button? integer; Returns: boolean
Mouse button 0..4; default 0.
imgui.is_item_hovered
imgui.is_mouse_clicked
imgui.is_mouse_clicked(button)
button? integer; Returns: boolean
Mouse button 0..4; default 0.
imgui.is_mouse_down
imgui.is_mouse_down(button)
button? integer; Returns: boolean
Mouse button 0..4; default 0.
imgui.is_mouse_released
imgui.is_mouse_released(button)
button? integer; Returns: boolean
Mouse button 0..4; default 0.
imgui.item_rect
imgui.mouse_delta
imgui.mouse_pos
imgui.open_popup
imgui.popup
imgui.progress
imgui.radio_button
imgui.same_line
imgui.selectable
imgui.selectable(label,selected,width,height)
label string; selected boolean; width? number; height? number; Returns: boolean
imgui.separator
imgui.separator()
imgui.set_cursor
imgui.set_cursor_screen
imgui.set_next_item_width
imgui.slider_float
imgui.slider_float(label,value,min,max,options)
label string; value number; min number; max number; options? {style?:string}; Returns: boolean, number
imgui.slider_int
imgui.slider_int(label,value,min,max,options)
label string; value number; min number; max number; options? {style?:string}; Returns: boolean, number
imgui.spacing
imgui.tab_bar
imgui.tab_item
imgui.table
imgui.table(id,columns,options,draw)
id string; columns integer; options table; draw function
One to sixteen columns; layout scope restores after errors.
imgui.table_headers_row
imgui.table_headers_row()
imgui.table_next_column
imgui.table_next_row
imgui.table_set_column
imgui.table_setup_column
imgui.table_setup_column(label,width_or_weight,fixed)
label string; width_or_weight? number; fixed? boolean
imgui.text
imgui.text_colored
imgui.text_size
imgui.text_wrapped
imgui.tooltip
imgui.tree
imgui.window
imgui.window_pos
imgui.window_size
imgui.with_clip_rect
imgui.with_clip_rect(x0,y0,x1,y1,draw)
x0 number; y0 number; x1 number; y1 number; draw function
imgui.with_font_size
imgui.with_id
imgui.with_style
imgui.with_style_vars
input.is_key_down
input.is_key_down(vk_code) -> boolean
vk_code integer; Returns: boolean
local held = input.is_key_down(0x56)
input.is_key_pressed
input.is_key_pressed(vk_code) -> boolean
vk_code integer; Returns: boolean
if input.is_key_pressed(0x56) then print('V') end
input.mouse_position
inventory.add
inventory.add(draft) -> decimal_id or nil,error
draft InventoryDraft; Returns: string?; Returns: string? error
local id=assert(inventory.add{category='weapon',definition=7,wear=.1,seed=1})
inventory.catalog
inventory.catalog(category,start=1,limit=100) -> entries,total
category string; start integer?; limit integer?; Returns: InventoryCatalogEntry[]; Returns: integer total
local entries,total=inventory.catalog('weapon',1,100)
inventory.categories
inventory.categories() -> string[]
Returns: string[]
for _,category in ipairs(inventory.categories()) do print(category) end
inventory.equip
inventory.equip(id,teams=3) -> true or nil,error
id string; teams integer?; Returns: boolean?; Returns: string? error
assert(inventory.equip(saved_id,1)) -- CT=1, T=2, both=3
inventory.finishes
inventory.finishes(category,definition,start=1,limit=100,all=false) -> entries,total
category string; definition integer; start integer?; limit integer?; all boolean?; Returns: InventoryFinish[]?; Returns: integer|string total_or_error
local finishes,total=inventory.finishes('weapon',7,1,100)
inventory.get
inventory.get(id) -> item or nil,error
id string; Returns: InventoryItem?; Returns: string? error
local item,err=inventory.get(saved_id)
inventory.items
inventory.items(start=1,limit=100) -> items,total
start integer?; limit integer?; Returns: InventoryItem[]; Returns: integer total
local owned,total=inventory.items(1,100)
inventory.remove
inventory.remove(id) -> true or nil,error
id string; Returns: boolean?; Returns: string? error
assert(inventory.remove(saved_id))
inventory.status
inventory.status() -> {local,native,apply,revision,sticker_slots}
Returns: table
local status=inventory.status();print(status['local'],status.native,status.apply)
inventory.unequip
inventory.unequip(id,teams=3) -> true or nil,error
id string; teams integer?; Returns: boolean?; Returns: string? error
assert(inventory.unequip(saved_id,3))
inventory.update
inventory.update(id,cosmetics) -> true or nil,error
id string; cosmetics InventoryCosmetics; Returns: boolean?; Returns: string? error
assert(inventory.update(saved_id,{wear=.2,custom_name='My item'}))
json.array
json.array(table={}) -> tagged table
value? table; Returns: table
Marks array/object intent; rejects foreign metatables and invalid key types.
print(json.encode(json.array()))
json.decode
json.decode(text, preserve_null=false) -> value
text string; preserve_null? boolean; Returns: any
Set preserve_null=true to retain null as json.null; default nil for compatibility.
local value=json.decode('{"health":100}')
json.encode
json.encode(value) -> string
value any; Returns: string
print(json.encode({health=100}))
json.object
json.object(table={}) -> tagged table
value? table; Returns: table
Marks array/object intent; rejects foreign metatables and invalid key types.
print(json.encode(json.object()))
mathx.angle_fov
mathx.angle_fov(pitch,yaw,target_pitch,target_yaw) -> number
pitch number; yaw number; target_pitch number; target_yaw number; Returns: number
local fov=mathx.angle_fov(0,0,10,20)
mathx.angle_vectors
mathx.angle_vectors(pitch,yaw,roll=0) -> forward,right,up
pitch number; yaw number; roll? number; Returns: CS2Vector, CS2Vector, CS2Vector
Forward/right/up basis; degrees.
local forward,right,up=mathx.angle_vectors(0,90)
mathx.approach
mathx.approach(current,target,max_step) -> number
current number; target number; max_step number; Returns: number
print(mathx.approach(0,10,2))
mathx.approach_angle
mathx.approach_angle(current,target,max_degrees) -> [-180,180)
current number; target number; max_degrees number; Returns: number
print(mathx.approach_angle(170,-170,5))
mathx.calc_angle
mathx.calc_angle(x1,y1,z1,x2,y2,z2) -> vector
x1 number; y1 number; z1 number; x2 number; y2 number; z2 number; Returns: CS2Vector
local angles=mathx.calc_angle(0,0,0,100,100,0)
mathx.clamp
mathx.clamp(value,min,max) -> number
value number; min number; max number; Returns: number
local hp=mathx.clamp(player.health,0,100)
mathx.damp
mathx.damp(current,target,rate,dt=engine.delta_time()) -> number
current number; target number; rate number; dt? number; Returns: number
Exponential damping; rate and dt nonnegative, dt defaults to engine.delta_time().
local next_value=mathx.damp(0,1,8,1/60)
mathx.distance
mathx.distance(x1,y1,z1,x2,y2,z2) -> number
x1 number; y1 number; z1 number; x2 number; y2 number; z2 number; Returns: number
local d=mathx.distance(0,0,0,3,4,0)
mathx.ease
mathx.ease(curve,t) -> [0,1]
curve string; t number; Returns: number
linear, in_quad, out_quad, in_out_quad, in_cubic, out_cubic, in_out_cubic, in_sine, out_sine, in_out_sine.
local progress=mathx.ease("in_out_cubic",.5)
mathx.lerp
mathx.lerp(a,b,t) -> number
a number; b number; t number; Returns: number
local alpha=mathx.lerp(0,1,.5)
mathx.lerp_angle
mathx.lerp_angle(current,target,t) -> [-180,180)
current number; target number; t number; Returns: number
Shortest arc; result normalized to [-180,180).
print(mathx.lerp_angle(170,-170,.5))
mathx.normalize_angle
mathx.normalize_angle(degrees) -> [-180,180)
degrees number; Returns: number
print(mathx.normalize_angle(270))
mathx.remap
mathx.remap(value,in_min,in_max,out_min,out_max) -> number
value number; in_min number; in_max number; out_min number; out_max number; Returns: number
local width=mathx.remap(player.health,0,100,0,160)
mathx.remap_clamped
mathx.remap_clamped(value,in_min,in_max,out_min,out_max) -> number
value number; in_min number; in_max number; out_min number; out_max number; Returns: number
print(mathx.remap_clamped(150,0,100,0,1))
mathx.seconds_to_ticks
mathx.seconds_to_ticks(seconds) -> nearest integer tick count
seconds number; Returns: integer
local ticks=mathx.seconds_to_ticks(.5)
mathx.smootherstep
mathx.smoothstep
mathx.ticks_to_seconds
mathx.ticks_to_seconds(integer_ticks) -> seconds
ticks integer; Returns: number
local seconds=mathx.ticks_to_seconds(32)
mathx.vector_angles
mathx.vector_angles(direction) -> angles or nil
direction CS2Vector; Returns: CS2Vector?
Pitch/yaw/zero roll; nil for the zero vector.
local angles=mathx.vector_angles(vector(1,1,0))
model_preview.draw
model_preview.draw(label,id_or_draft,width,height,transparent=false) -> {visible,ready,x1,y1,x2,y2} or nil,error
label string; id_or_draft string|InventoryDraft; width number; height number; transparent boolean?; Returns: table?; Returns: string? error
ui.tab('models','Models',function()
model_preview.draw('AK preview',{category='weapon',definition=7},320,280)
end)
model_preview.status
model_preview.status() -> {state,message}
Returns: table
print(model_preview.status().message)
native.bind
native.bind(address,signature)
address integer; signature CS2NativeSignature; Returns: fun(...):any?,string?
native.export
native.hook
native.hook(address,signature,callback_source)
address integer; signature CS2NativeSignature; callback_source string Returns function(original,...); isolated worker, no parent closures/UI.; Returns: CS2NativeHook?,string?
native.module
native.read
native.read(address,type)
address integer; type CS2NativeType; Returns: boolean|number|nil,string?
native.read_bytes
native.read_bytes(address,length)
address integer; length integer 1..4096; Returns: string?,string?
native.relative
native.relative(address,displacement_offset,instruction_length)
address integer; displacement_offset? integer; instruction_length? integer; Returns: integer?,string?
native.scan
native.scan(module,pattern,occurrence)
module string; pattern string Space-separated hex and ?/?? bytes, up to 256 bytes.; occurrence? integer 0 requires a unique match; positive values are one-based.; Returns: integer?,string?
native.vtable
native.vtable(object_address,zero_based_index)
object_address integer; zero_based_index integer; Returns: integer?,string?
rect
render.arc
render.arc(x,y,radius,start_degrees,end_degrees,color,thickness=1,segments=48)
x number; y number; radius number; start_degrees number; end_degrees number; color CS2Color|number[]; thickness? number; segments? integer
events.on('render',function()
render.arc(90,90,30,-90,180,color(.2,.7,1),3)
end)
render.bezier
render.bezier(x1,y1,x2,y2,x3,y3,x4,y4,color,thickness=1,segments=32)
x1 number; y1 number; x2 number; y2 number; x3 number; y3 number; x4 number; y4 number; color CS2Color|number[]; thickness? number; segments? integer
events.on('render',function()
render.bezier(20,90,60,10,120,10,160,90,color(1,.5,.2),2)
end)
render.circle
render.circle(x,y,radius,rgba,filled,thickness)
x number; y number; radius number; rgba CS2Color|number[]; filled? boolean; thickness? number
render.gradient
render.gradient(x,y,w,h,rgba_top,rgba_bottom)
x number; y number; w number; h number; rgba_top CS2Color|number[]; rgba_bottom CS2Color|number[]
render.gradient(20,20,160,40,{.2,.6,.8,1},{.1,.1,.1,1})
render.line
render.line(x0,y0,x1,y1,rgba,thickness)
x0 number; y0 number; x1 number; y1 number; rgba CS2Color|number[]; thickness? number
render.measure_text
render.measure_text(text,size=14) -> width,height
text string; size? number; Returns: number, number
local w,h=render.measure_text('CS2',14)
render.polyline
render.polyline({{x,y},...},color,thickness=1,closed=false)
points number[][]; rgba CS2Color|number[]; thickness? number; closed? boolean
render.polyline({{10,10},{30,30},{50,10}},{1,1,1,1})
render.rect
render.rect(x,y,width,height,rgba,filled,rounding)
x number; y number; width number; height number; rgba CS2Color|number[]; filled? boolean; rounding? number
render.scale
render.screen_size
render.screen_size() -> width,height
Returns: number,number
local width,height=render.screen_size()
render.text
render.text(x,y,text,rgba,size)
x number; y number; text string; rgba CS2Color|number[]; size? number
render.theme
render.theme() -> {accent,text,muted,panel,border}
Returns: {accent:CS2Color,text:CS2Color,muted:CS2Color,panel:CS2Color,border:CS2Color}
local colors=render.theme()
render.triangle
render.triangle(x1,y1,x2,y2,x3,y3,color,filled=true)
x1 number; y1 number; x2 number; y2 number; x3 number; y3 number; rgba CS2Color|number[]; filled? boolean
render.triangle(20,20,40,20,30,40,{1,1,1,1})
render.world_to_screen
render.world_to_screen(x,y,z) or render.world_to_screen(vector) -> x,y or nil
x number; y number; z number; Returns: number?,number?
local x,y=render.world_to_screen(p.origin.x,p.origin.y,p.origin.z)
script.name
settings.get
settings.get(feature_id) -> value
feature_id string; Returns: any
print(settings.get('espEnabled'))
settings.info
settings.info(feature_id) -> metadata or nil
feature_id string; Returns: CS2Setting?
print(json.encode(settings.info('espEnabled')))
settings.list
settings.list(filter='') -> metadata[]
filter? string; Returns: CS2Setting[]
for _,f in ipairs(settings.list('esp')) do print(f.id,f.type) end
settings.set
settings.set(feature_id, value)
feature_id string; value any
settings.set('espEnabled',true)
storage.delete
storage.read
storage.read(key, fallback=nil, preserve_null=false) -> value
key string; fallback? any; preserve_null? boolean; Returns: any
local prefs=storage.read('preferences',{enabled=true})
storage.write
storage.write(key, JSON-compatible value)
key string; value any
storage.write('preferences',{enabled=true})
timers.after
timers.after(seconds, callback) -> id
seconds number; callback function; Returns: integer
timers.after(2,function() print('ready') end)
timers.cancel
timers.every
timers.every(seconds, callback) -> id
seconds number; callback function; Returns: integer
local id=timers.every(1,function() print(cs2.clock().tick) end)
ui.button
ui.button(label,width,height)
label string; width? number; height? number; Returns: boolean
ui.columns
ui.combo
ui.combo(label,index,items)
label string; index integer; items string[]; Returns: boolean, integer
One-based index; 1..128 items.
ui.entity_colors
ui.entity_colors()
Returns: boolean
Availability probe. CS2 uses settings.get/set/list for native ESP colors.
Returns false in CS2; use settings for CS2 colors.
ui.feature
ui.feature_visible
ui.group
ui.keybind
ui.multi_combo
ui.multi_combo(label,selected,items)
label string; selected boolean[]; items string[]; Returns: boolean, boolean[]
Dense selection array with one entry per item.
ui.next_column
ui.next_column()
ui.overlay
ui.override
ui.override(page,draw)
page string; draw function; Returns: string
CS2 supports lua/ui and lua/api only.
ui.reset_theme
ui.reset_theme()
ui.selectable
ui.selectable(label,selected,width,height)
label string; selected boolean; width? number; height? number; Returns: boolean
ui.slider
ui.slider(label,value,min,max,options)
label string; value number; min number; max number; options? {style?:string}; Returns: boolean, number
ui.slider_int
ui.slider_int(label,value,min,max,options)
label string; value number; min number; max number; options? {style?:string}; Returns: boolean, number
ui.subtab
ui.subtab(parent,id,label,draw)
parent string; id string; label string; draw function; Returns: string
ui.tab
ui.tab(id,label,draw,metadata)
id string; label string; draw? function|table; metadata? table; Returns: string
Register during script loading. Metadata may be third argument if there is no draw callback.
ui.theme
ui.toggle
ui.tr
ui.window
ui.window(id,title,options,draw)
id string; title string; options table; draw function; Returns: string
ui.window_visible
utils.base64_decode
utils.base64_decode(text) -> bytes
text string; Returns: string
Strict alphabet and canonical Base64 padding; output at most 65536 bytes.
print(utils.base64_decode("aGVsbG8="))
utils.base64_encode
utils.base64_encode(bytes) -> string
bytes string; Returns: string
Binary strings supported; at most 65536 input bytes.
print(utils.base64_encode("hello"))
utils.fnv1a
utils.fnv1a(bytes) -> unsigned 32-bit integer
bytes string; Returns: integer
FNV-1a 32-bit hash, not cryptographic.
print(utils.fnv1a("hello"))
utils.from_bytes
utils.from_bytes(byte[]) -> string
bytes integer[]; Returns: string
Dense array of integer bytes 0..255.
print(utils.from_bytes({72,105,0}))
utils.hex_decode
utils.hex_decode(hex) -> bytes
text string; Returns: string
Strict alphabet and canonical Base64 padding; output at most 65536 bytes.
print(utils.hex_decode("68656c6c6f"))
utils.hex_encode
utils.hex_encode(bytes) -> lowercase hex
bytes string; Returns: string
Binary strings supported; at most 65536 input bytes.
print(utils.hex_encode("hello"))
utils.to_bytes
utils.to_bytes(string) -> byte[]
bytes string; Returns: integer[]
local bytes=utils.to_bytes("hello")
utils.unix_time
utils.unix_time() -> integer seconds since Unix epoch
Returns: integer
print(utils.unix_time())
vector
vector2
widgets.button
widgets.button(label,width,height)
label string; width? number; height? number; Returns: boolean
Alias: ui.widgets.button.
widgets.columns
widgets.columns(count,draw,compact)
count integer; draw function; compact? boolean
Alias: ui.widgets.columns.
widgets.combo
widgets.combo(label,index,items)
label string; index integer; items string[]; Returns: boolean, integer
One-based index; 1..128 items.
Alias: ui.widgets.combo.
widgets.group
widgets.multi_combo
widgets.multi_combo(label,selected,items)
label string; selected boolean[]; items string[]; Returns: boolean, boolean[]
Dense selection array with one entry per item.
Alias: ui.widgets.multi_combo.
widgets.next_column
widgets.selectable
widgets.selectable(label,selected,width,height)
label string; selected boolean; width? number; height? number; Returns: boolean
Alias: ui.widgets.selectable.
widgets.slider
widgets.slider(label,value,min,max,options)
label string; value number; min number; max number; options? {style?:string}; Returns: boolean, number
Alias: ui.widgets.slider.
widgets.slider_int
widgets.slider_int(label,value,min,max,options)
label string; value number; min number; max number; options? {style?:string}; Returns: boolean, number
Alias: ui.widgets.slider_int.
widgets.toggle
widgets.toggle(label,value)
label string; value boolean; Returns: boolean, boolean
Alias: ui.widgets.toggle.
Coverage & porting
Download MarkdownCS2 API coverage and porting
Reviewed 2026-09-30 against the public Fatality Lua 2 reference, Scooby host 2.4 and UI 1.1. This is a capability comparison, not drop-in compatibility. The executable contract is Scooby's host API, editor metadata and regression fixtures. Similar features can have different names, callback timing, types and error behavior.
Available equivalents
| Reference area | Scooby API | Differences |
|---|---|---|
| Utility functions | utils.base64_encode/decode, utils.to_bytes/from_bytes, utils.hex_encode/decode, utils.fnv1a, utils.unix_time |
Snake-case names; strict bounded binary conversions. Murmur2, date tables and script clipboard access are not exposed. |
| Pattern/export discovery | native.scan, native.export, native.module |
Typed Win64 binding/hooking; scans reject ambiguous matches by default. |
| JSON/files | json, files, storage, fs |
Script-scoped nested folders, atomic saves and opt-in text-module loading; no arbitrary external paths. JSON null and container tags are explicit. |
| Math | vector, vector2, rect, color, colors, mathx, render.world_to_screen |
Source-unit vectors, degree angles, normalized RGBA. Adds easing and frame-independent damping. |
| GUI / controls | ui, imgui, widgets / ui.widgets, features |
Immediate-mode callback scopes; use CS2 settings IDs. This does not implement the reference's retained GUI objects. |
| Drawing | render, draw |
Text, shapes, gradients, arcs, Beziers and window clipping. Texture/font/shader/SVG/animated-resource objects are not exposed. |
Partial engine coverage
| Area | Current implementation | Missing integration |
|---|---|---|
| Entities | Copied player/weapon snapshots, validated handles, selected bones and view state | General entity classes, writable schema properties, hitboxes and full weapon metadata |
| Events | update/render/shutdown plus sampled session, health, death, spawn and local-weapon changes | Native server-event payloads and frame-stage/view hooks. commands.create now provides isolated command mutation. |
| Native hooks | Fixed Windows x64 scalar calls, detours and isolated worker state | Struct/vector ABI, varargs, LuaJIT FFI and automatic engine SDK bindings |
Sampled health changes do not identify an attacker, hitgroup or authoritative damage event. A native address does not make an engine service available: it still needs a verified prototype, valid objects, the correct thread and an actual host implementation.
Not exposed
Command-worker shot traces and asynchronous HTTP are exposed with permission/readiness checks. General physics queries, penetration, particle creation, schema reflection, WebSocket and Panorama APIs remain unavailable. Native hooks are not a drop-in implementation of those systems. Script clipboard/date/Murmur2 helpers, managed GPU resources and retained GUI objects are additional gaps. cs2.capabilities() reports explicit false entries for the major unavailable categories.
Shared esp_colors functions are present for interface consistency but CS2 has no shared entity-color provider. esp_colors.available() and ui.entity_colors() return false; use settings.list, settings.info, settings.get and settings.set for the native CS2 controls.
Port a script
- Replace external module names and retained GUI objects with documented Scooby bindings and UI callback scopes.
- Replace assumed feature IDs with values returned by
settings.list(). - Check optional player/projection results for nil and preserve the full player handle.
- Replace native-event assumptions with explicitly sampled behavior only when that behavior is sufficient.
- Use the Console's script filter, Errors only and Copy visible to diagnose failures. Keep initialization outside per-frame callbacks.
The current implementation deliberately reports unsupported capabilities rather than publishing placeholder functions. Next engine work should add verified event payloads, general entity/schema access one service at a time, with thread/lifetime rules and real in-game validation. Native inventory changes and item model previews are available; arbitrary GPU resources and Panorama require further implementation. Command/native model behavior still needs in-game acceptance on the installed build.
Keeping documentation complete
python tools/check_lua_api.py --write refreshes the binding catalog/index from current registration plus reviewed annotations. The check without --write rejects missing editor declarations, undocumented exports and stale output. --runtime build/LuaCoverage20260930/lua.log compares that inventory against functions enumerated by the real Lua fixture. The site generator imports the host contract, binding index, comparison guide and editor metadata; its --check mode rejects stale output.
Shared UI API
Download MarkdownLua UI API
Version 1.1 (shared by V1 and V2). UI_API_VERSION is "1.1".
This is the portable UI API for Scooby Simple Base. It provides persistent scripts, custom features, tabs, sub-tabs, independent menus, overlays, page replacements, theming, and a curated set of ImGui controls. Game-specific functions are supplied by the project that embeds the UI.
Open API docs from the Lua editor for the API browser beside the main GUI. V1 uses the editor documentation button; V2 uses the toggle beside Stop. Expand sections and API members to view signatures and code examples. The browser uses the main UI's theme, fonts, spacing and editor colors; explanatory notes and sections containing only notes are omitted. It supports mouse navigation, independent resizing, selection/copy and horizontal code scrolling. Use the close button or Escape while focused to dismiss it. The browser loads this reference with the host's game-specific extension; optional docs/LUA_SNIPPETS.md adds examples. The preview supports --page api --no-welcome.
The source reference is docs/LUA_API.md; consumer and preview builds automatically stage it at assets/docs/LUA_API.md beside the executable.
Getting started
- Open Lua > Scripts, select Custom UI.lua, and click Run.
- A My Tools tab appears in the sidebar. Its controls edit real registered features.
- F8 toggles its feature and the example overlay.
- Run Standalone Menu.lua to try an independent menu; F9 toggles its visibility.
- Use Stop next to the editor's script selector to unload that script.
The independent menus are ImGui windows rendered inside the host's viewport. They can run while the main GUI is closed. This API does not create a separate operating-system process or desktop window.
Minimal custom tab:
local enabled = features.add {
id = "enabled", label = "My feature", default = false,
category = "My Tools", description = "My custom feature", key = "F8"
}
local tab = ui.tab("tools", "My Tools")
ui.subtab(tab, "general", "General", function()
ui.group("Options", function()
ui.feature(enabled)
end)
end)
Localization
ui.tr(english_key) returns the selected catalog's logical UTF-8 text, or the original key when absent. It is available during loading and callbacks. Call it inside a callback when text must follow live language changes.
Built-in Lua text, buttons, groups, sliders, dropdowns, tooltips, input/color labels, window titles and render.text translate their visible captions automatically. Widget IDs continue to use the original labels. Keep labels stable; add an entry with that English key to each languages/<code>.json catalog to localize a custom script. Player names and arbitrary values are not translation keys.
Format translated templates before drawing; Arabic/Hebrew display shaping happens afterwards:
ui.subtab("settings", "localized_stats", "Info", function()
imgui.text(string.format(ui.tr("Speed: %.0f units/s"), 250))
end)
Do not cache ui.tr results at script load if the text should change with the selected language. Keep %s, %d, and other format specifiers unchanged in translated templates. Missing keys intentionally fall back to English. The API guide and console/game diagnostics remain developer text.
Lua console diagnostics
Open Lua > Console, or the windowed console, after a script fails. Reports identify the script and the failure phase: compile, startup, events.update/events.draw/events.shutdown, a feature action, or the UI callback ID. Syntax errors include the source line; runtime failures include a Lua traceback with nested calls.
[ERROR] [Lua error] My overlay | events.update (runtime)
My overlay.lua:12: attempt to index a nil value
stack traceback:
My overlay.lua:12: in local 'read_player'
My overlay.lua:20: in function <My overlay.lua:19>
This callback was disabled. Fix the error and run the script again.
The console timestamp and [ERROR] severity are added by the host. A callback failure is reported once and that callback is disabled; other scripts continue. Fix the reported line and run the script again. Startup failures remove resources registered by that attempt.
- Nil value or missing field: check the API name, host version and session state; functions may return
nilwhile loading. - Syntax: inspect the named line and the preceding statement for missing
end, quotes, commas or brackets. - Execution budget: split long loops over updates; avoid blocking work and excessive nested callbacks.
- Memory limit (16 MiB per script): reduce retained tables/strings and large allocations.
- Error object: use
error("description")orerror({message="description"})for useful text. Error reporting does not invoke custom__tostringfunctions.
Native hooks use a separate 2 MiB VM and report their native address and fallback behavior in addition to the traceback. See the host's native-hook reference for those limits.
Script lifetime and ownership
Each named script has its own Lua state. Top-level code runs once. Local variables captured by callbacks survive between frames. Different scripts have separate global variables.
Running the same script name again stops its previous instance before loading the replacement. Syntax or startup errors leave the replacement stopped. The old instance is not restored. Running another name leaves existing scripts running.
Stopping removes the script's tabs, sub-tabs, windows, overlays, page replacements, themes, visibility overrides and registered features, including their reset defaults. It closes its state after calling shutdown callbacks. Values explicitly written to existing host features with features.set, features.bind, features.color or base.set remain changed.
Register features, pages, windows, overlays and event callbacks at the top level. Registration from a frame callback is rejected, preventing callbacks from invalidating UI/feature iteration.
A drawing callback that errors is logged and disabled. A failed event or action callback is disabled too. Other scripts continue. Reload the script after correcting the error.
The script name determines its namespace. The editor uses the selected filename without .lua; an unsaved editor uses Untitled. The C++ runtime's default name is editor. Renaming a file changes its namespace on its next run; stop the old instance if it is still running.
Features
A registered feature joins the same registry as built-in features. It participates in search, favorites, hotkeys, config values, and the Active Features overlay. A Lua feature supplies state and optional action callbacks; gameplay behavior belongs in a project API or an update callback.
Register a feature
local id = features.add {
id = "example", -- required local ID, 1-80 characters
label = "Example", -- defaults to local ID
category = "My Tools / General", -- defaults to "Scripts"
description = "What this does",
default = false, -- initial enabled state
key = "F8", -- optional key, Toggle mode
active_list = true, -- include effective toggles in Active Features
kind = "toggle" -- "toggle" or "action"
}
The returned ID is lua.<script name>.<local id>. Keep and use the returned ID rather than constructing it. Duplicate IDs are rejected.
An action runs once per button/hotkey activation:
local action = features.add {
id = "refresh", label = "Refresh", kind = "action",
on_trigger = function() print("Refresh requested") end
}
Read and change features
| Function | Behavior |
|---|---|
features.get(id) |
Returns the saved enabled boolean, or nil for an unknown ID. |
features.set(id, boolean) |
Changes enabled state; rejects unknown IDs. |
features.active(id) |
Returns effective hotkey state, including Hold/Hold Off. |
features.trigger(id) |
Activates an action, including its callback and overlay flash. |
features.bind(id, key, mode) |
Sets a key and mode. Empty key clears the key. |
features.color(id, r, g, b, a) |
Sets RGBA color; components must be 0-1. Alpha defaults to 1. |
features.list() |
Returns an array of tables with id, label, category, description, kind, enabled. |
ui.feature(id) |
Draws the standard feature row, including applicable gear/color/hotkey controls. |
ui.keybind(id [, label]) |
Draws shared capture, Clear and activation mode for an existing feature; drawing callbacks only. Label defaults to Key. |
Binding modes are "always", "toggle", "hold", and "hold_off". Key names match the menu's binding names, for example F8, G, Mouse 1 and Insert. Hotkeys follow the host's focus and text-input rules.
features.active should gate behavior. features.get intentionally returns saved state and does not resolve Hold modes.
Legacy compatibility: base.get(id), base.set(id, boolean), base.log(...) and print(...) remain available.
Independent health overlays
features.set("esp.health", true)
features.set("esp.health_text", true)
features.bind("esp.health_text", "F7", "toggle")
ui.tab("health_overlays", "Health", function()
ui.feature("esp.health")
ui.feature("esp.health_text")
end)
esp.health is the existing bar; esp.health_text is independent HP text using the same config/hotkey services. Both require authoritative host health. Text can render without a maximum; bars require a valid maximum. See the host health contract.
Inline key capture
ui.tab("aim_controls", "Aimbot", function()
ui.group("Activation", function()
ui.feature("host.aim")
ui.keybind("host.aim", "Aim key")
end)
end)
Use an existing registered host feature ID. Release initiating inputs before assigning a key or mouse button; Escape cancels and Delete clears. The control edits the same binding as features.bind and Hotkeys. Hosts can opt into separate saved master and transient activation; see the integration contract. Aim behavior modes remain separate from activation modes.
Tabs and sub-tabs
local tab = ui.tab("tools", "My Tools")
local subtab = ui.subtab(tab, "display", "Display", function()
imgui.text("My page")
end)
ui.tab(id, label [, draw]) creates a sidebar entry and returns its opaque ID. An optional draw callback becomes its Overview page. A tab without a draw callback selects its first sub-tab.
ui.subtab(parent, id, label, draw) creates a sub-tab and returns its ID. Parent can be a tab ID returned by ui.tab, or one of the built-in IDs: "visuals", "lua", "settings".
ui.subtab("settings", "my_settings", "My Script", function()
imgui.text("An extra page beside the built-in settings.")
end)
IDs must be nonempty and unique across a script's UI registrations. Labels can be changed independently. Custom sidebar entries scroll when they exceed the available height.
Groups and columns
Scoped helpers always close their native ImGui/group state, including when a nested Lua callback fails. There are no manual Begin/End or Push/Pop pairs to balance.
ui.columns(3, function()
ui.group("First", function() imgui.text("First column") end)
ui.next_column()
ui.group("Second", function() imgui.text("Second column") end)
ui.next_column()
ui.group("Third", function() imgui.text("Third column") end)
end)
ui.group(label, draw) creates a collapsible Scooby card. ui.columns(count, draw) supports 1-5 columns and reflows on narrow windows. ui.next_column() advances inside that scope. Nested column layouts are rejected. Use unique group labels or ##suffix IDs for repeated controls within a callback.
ImGui controls
imgui is a curated immediate-mode binding to the bundled ImGui renderer, not a claim to expose every upstream ImGui function. Call these functions from tab, sub-tab, window, overlay, or page replacement callbacks. Calling drawing functions while loading or from an update event returns a Lua error.
Controls keep values in Lua and return the edited value:
local amount, enabled, text = 50, false, "Hello"
ui.window("example", "Controls", {}, function()
local changed
changed, enabled = imgui.checkbox("Enabled", enabled)
changed, amount = imgui.slider_float("Amount", amount, 0, 100)
changed, text = imgui.input_text("Text", text)
end)
| Function | Return / behavior |
|---|---|
imgui.text(text) |
Unwrapped text; treats the string as text, not a printf format. |
imgui.text_colored(text, rgba) |
Colored text. |
imgui.button(label [, width, height]) |
Returns true on activation; dimensions default to automatic. |
ui.selectable(label, selected [, width, height]) |
Plain text row using the host selection highlight. Returns true on mouse activation; selection remains caller-owned. Zero dimensions use available width and text height. |
imgui.checkbox(label, value) |
Returns changed, boolean. |
imgui.slider_float(label, value, min, max[, options]) |
Returns changed, number. Optional {style="track"} uses a slim track and separate value. |
imgui.slider_int(label, value, min, max[, options]) |
Returns changed, integer-valued number. Optional {style="track"} uses a slim track and separate value. |
imgui.input_text(label, text) |
Returns changed, string; maximum 4095 bytes. |
imgui.combo(label, index, items) |
Returns changed, selected index. Indices start at 1; 1-128 items. |
imgui.color_edit(label, rgba) |
Returns changed, RGBA table. |
imgui.same_line([spacing, local_x]) |
Places the next item on the same line; default style spacing. |
imgui.separator() |
Separator line. |
imgui.spacing([height]) |
Vertical space; default 4 physical pixels. |
imgui.tooltip(text) |
Tooltip when the preceding item is hovered. |
imgui.progress(fraction) |
Progress bar; fraction is clamped to 0-1. |
imgui.available() |
Returns available content width, height. |
imgui.cursor() |
Returns cursor x, y in screen coordinates. |
imgui.set_cursor(x, y) |
Sets the next cursor position in window-local coordinates. |
imgui.is_item_hovered() |
Returns whether the preceding item is hovered. |
imgui.child(id, width, height, draw [, options]) |
Scoped scrollable child; 0 uses remaining size. Options: border, padding (retain WindowPadding without a visible border), horizontal_scroll. |
imgui.disabled(boolean, draw) |
Scoped disabled controls. |
imgui.with_style(colors, draw) |
Scoped color overrides, restored even after callback errors. |
imgui.window(title, options, draw) |
Scoped independent ImGui window. |
RGBA values use {r, g, b, a} with components between 0 and 1. Alpha defaults to 1 when omitted. Use stable ##suffix labels when controls have duplicate visible names.
Lua local variables are live session state; they are not automatically written to profiles. Registered feature values participate in the existing profile system. Register the scripts before loading a profile containing their feature IDs.
Selectable catalog rows
local selected = 1
local catalog = {"Crowbar", "Health kit"}
ui.tab("catalog", "Spawner", function()
ui.group("Items", function()
for index, label in ipairs(catalog) do
if ui.selectable(label .. "##item_" .. index, selected == index) then
selected = index
end
end
imgui.disabled(true, function()
ui.selectable("Unavailable item", false)
end)
end)
if ui.button("Spawn item") then
print("Spawn requested for " .. catalog[selected])
-- Invoke the host's actual spawn API here.
end
end)
The row has no button border or idle box. selected is a required boolean; Lua keeps selection state and changes it only when activation returns true. Mouse clicks activate rows; keyboard navigation is disabled, and imgui.disabled blocks activation. Use stable hidden ID suffixes for repeated labels. Dimensions are optional nonnegative finite physical pixels; 0 fills available width or uses text height. Drawing-context validation is identical to other controls. Selecting a catalog row must not call a spawn API; the separate Spawn item action uses the current selection.
Overlays and drawing
ui.overlay("status", function()
local width, height = engine.viewport()
render.rect(20, height - 60, 240, 36, {0.05, 0.07, 0.09, 0.9}, true, 4)
render.text(30, height - 51, "Lua overlay", {0.4, 0.8, 1, 1}, 14)
end)
ui.overlay(id, draw) invokes draw every frame, including when the main GUI is closed. Render primitives use the foreground draw list in the current host viewport. Coordinates and sizes are physical pixels; text size is rounded to a whole pixel. A script can derive responsive positions from engine.viewport().
| Function | Arguments |
|---|---|
render.text(x, y, text, rgba [, size]) |
Text; size defaults to 13 and is limited to 8-80. |
render.line(x1, y1, x2, y2, rgba [, thickness]) |
Line; default thickness 1. |
render.rect(x, y, width, height, rgba [, filled, rounding]) |
Filled or outlined rectangle; default outline, rounding 0. |
render.circle(x, y, radius, rgba [, filled, thickness]) |
Filled or outlined circle; default outline, thickness 1. |
Custom draw-list overlays do not acquire a built-in drag handle. Use ui.window for an interactive draggable overlay, or implement interaction in the project-specific host API.
Themes and UI overrides
ui.theme {
rounding = 5,
colors = {
WindowBg = {0.06, 0.07, 0.1, 1},
Text = {0.9, 0.92, 1, 1},
CheckMark = {0.75, 0.4, 1, 1},
SliderGrab = {0.75, 0.4, 1, 1},
Button = {0.22, 0.15, 0.32, 1},
ButtonHovered = {0.32, 0.22, 0.45, 1}
}
}
ui.theme(options) installs or replaces this script's theme overrides. Color names match ImGui names without ImGuiCol_, such as WindowBg, PopupBg, Text, TextDisabled, CheckMark, SliderGrab, FrameBg, FrameBgHovered, Button, Header and Border. Unknown names are rejected. Rounding sets WindowRounding and FrameRounding, from 0 to 24 pixels.
Overrides apply on the next frame after the user's baseline theme. The most recently applied script theme wins for overlapping fields. ui.reset_theme() or stopping the script restores the underlying theme. Scoped imgui.with_style({Text={1,0,0,1}}, draw) affects only the callback's controls. Some custom card/overlay surfaces have their own drawing colors; this API edits the ImGui palette rather than every game-specific renderer constant.
ui.feature_visible(featureId, boolean) hides/shows the standard row and its search results for that script's lifetime. It does not enable/disable the feature's behavior. If multiple scripts hide a feature, all must release the override before it becomes visible.
Replace the contents of a built-in page:
ui.override("settings/interface", function()
imgui.text("My replacement settings page")
if imgui.button("Log") then print("Replacement works") end
end)
Supported targets:
visuals/esp,visuals/radarlua/scripts,lua/editor,lua/consolesettings/interface,settings/config,settings/hotkeys,settings/overlays,settings/links
The shell, search and navigation remain available. The latest registered replacement wins. Stopping its script reveals the previous replacement or original page. A failed replacement is disabled so the original page can recover on the next frame.
Events and host information
events.on("update", function()
-- Read feature state and call a project-provided API here.
end)
events.on("shutdown", function()
print("Cleaning up my script")
end)
update runs once per rendered application frame. shutdown runs when the script is stopped or replaced and during application shutdown. Both use protected Lua calls. They cannot draw ImGui widgets; register a UI callback for drawing.
| Function | Return |
|---|---|
engine.time() |
ImGui elapsed time in seconds. |
engine.delta_time() |
Frame delta in seconds. |
engine.fps() |
ImGui's smoothed frame rate. |
engine.viewport() |
Host viewport width, height. |
print(...), base.log(...) |
Write to the shared console. |
Frame updates run before the current frame's hotkey update, so features.active inside update sees the last processed hotkey state. Draw callbacks run after hotkeys and see the current state.
Porting and game-specific APIs
The default API deliberately contains UI and feature-registry behavior. It does not invent game/entity/memory/network APIs. Add the functions appropriate to each project through Application::scripts().registerHostApi before running scripts. The UI layer installs its own API separately, so the host hook does not replace it. A host that owns per-script resources can install removeHostApi(lua_State*) to release them when a state closes, including stop, reload and failed initial execution. The owner of these hooks must outlive the Application.
A minimal host extension:
extern "C" {
#include "lua.h"
#include "lauxlib.h"
}
app.scripts().registerHostApi = [](lua_State* L) {
lua_newtable(L);
lua_pushcfunction(L, [](lua_State* state) -> int {
lua_pushstring(state, "My Game");
return 1;
});
lua_setfield(L, -2, "name");
lua_setglobal(L, "game");
};
Lua can then call game.name() from its loading code or callbacks. For real game features, expose validated functions such as entity snapshots, local-player state, permitted actions or typed settings through the same hook. Connect live behavior in an update callback or the host feature dispatcher, using features.active(id) to honor hold/toggle bindings.
Thread and lifetime rules:
- Construct, run, stop and draw scripts on the application's UI/render thread.
- Do not call ImGui or the Lua state from background threads. Marshal host results to the render thread.
- Register host features before loading profiles. Run scripts before loading profiles containing script-owned IDs.
- A state remains alive until its named script is stopped/replaced or application shutdown. Do not retain it beyond that point.
- Call
app.shutdown()before destroying ImGui or host objects referenced by callbacks. - The bundled Lua target is compiled in C++ exception mode with a C-compatible public ABI, so protected errors unwind C++ bindings safely. Preserve that build mode when porting. On MSVC, bindings require
/EHswith/EHc-so C ABI calls may unwind; the CMake target propagates these options. - Native host calls must validate arguments and manage their own cancellation; Lua's instruction hook cannot preempt a blocking native function.
C++ lifecycle controls:
app.scripts().run(source, app.features(), logCallback, "My Script");
auto names = app.scripts().running();
app.scripts().stop("My Script");
app.scripts().stopAll();
app.navigateScript("lua:My Script:tools", "lua:My Script:general");
app.setApiDocsVisible(true, false); // true = attach left, false = attach right
Application supplies frame dispatch automatically. A core-only host using ScriptRuntime directly calls beginFrame() and dispatch("update") itself. Such a host gets features/events/base APIs; ImGui/UI/render/engine APIs are installed by the UI layer.
Limits and troubleshooting
The runtime enables Lua base, string, table, math and UTF-8 libraries. OS, file, package, debug, dynamic loading and coroutine libraries are not provided by default.
Limits: 1 MB source per run; 16 MB Lua allocator per state; 16 running scripts; 128 features per script; 128 total UI registrations; 32 callbacks per event per script; 512 drawing API calls per drawing callback. Loading has a 200 ms instruction-hook deadline; callbacks share an 8 ms per-script frame budget. These are responsiveness guards for scripts, not a security boundary for native extensions.
If a script fails, read Lua > Console. Error messages include the script name. Correct the code and rerun it. A drawing function called from top-level loading or update is rejected; move it into a registered draw callback. A stopped script's IDs are no longer valid. API registrations must use unique local IDs and happen during loading.
Profiles do not automatically start scripts. Save a script in the browser and run it explicitly. Register it again before loading profiles with its feature values. Arbitrary Lua locals are not persisted across reloads.
Host tabs may specify navigation metadata: ui.tab("world", "World", {section="Visuals", icon=0xe231}). With a page callback, pass metadata as argument 4. Default groups are Visuals and Tools; other group names appear between them. Hosts can arrange sections and built-in/script tab IDs with Application::setSidebarLayout. Unlisted script tabs are appended to their declared section, so user pages remain reachable. The L4D host uses Combat, Visuals and Misc. Icons use the bundled Lucide private-use codepoints. Set hidden=true in tab metadata to hide its sidebar entry; the host can still open the registered page with Application::navigateScript.
Custom product controls
ui.slider(label,value,min,max) and ui.slider_int(label,value,min,max) return changed,value and use the compact Lumia track and editable value control. Signed/zero ranges are supported. ui.toggle(label,value) returns changed,value; ui.combo(label,index,items) returns changed,index with a one-based index. ui.button(label[,width,height]) returns a boolean. These controls follow the active template scale, colors and disabled scope.
API 1.1 uses native Dear ImGui implementations for imgui.button, imgui.checkbox, imgui.slider_float, imgui.slider_int, imgui.combo and imgui.selectable. Existing ui.* controls retain the Simple Base appearance. Use widgets.* (also available as ui.widgets.*) to explicitly request that appearance in any script. Lua owns control values in either family; return shapes and one-based combo indices are unchanged. Native controls respect imgui.set_next_item_width and the current style, without forcing full width.
ui.multi_combo(label, selected, items) returns changed, selected for 1-128 items. Pass a dense boolean array with one entry per item; the returned array is new and the input is unchanged. Selected rows are highlighted without checkboxes. Clicking an entry toggles only that entry and keeps the dropdown open; click outside or press Escape to close it. Labels and the None/All summary follow the active language. Save selections through feature/config values when persistence is needed.
local values = {features.get("aim.common"), features.get("aim.special")}
local changed, selected = ui.multi_combo("Targets", values, {"Common infected", "Special infected"})
if changed then
features.set("aim.common", selected[1])
features.set("aim.special", selected[2])
end
Shared settings and hotkey popups use the same custom buttons and dropdowns. Native hosts can use ui::beginCombo, ui::comboItem and ui::endCombo for dynamic lists. Negative button widths fill the remaining row, matching the existing layout convention.
ui.columns(count, draw [, compact]) accepts an optional boolean for tighter columns. The default is unchanged; true uses a smaller readable minimum width before reflowing on narrow windows.
Entity colors
| Signature | Behavior |
|---|---|
esp_colors.available() |
Returns whether the host registered classification color support. |
esp_colors.categories() |
Returns the 13 stable category IDs in display order. |
esp_colors.enabled([boolean]) |
Reads or sets the category master. Unsupported hosts return false; writes fail. |
esp_colors.get(category) |
Returns enabled, color, second_color and tint_fill; returns nil without host support. |
esp_colors.set(category, settings) |
Atomically applies supplied fields. RGBA arrays require exactly four finite numbers from 0 to 1. |
esp_colors.reset([category]) |
Resets one category, or all categories and the master when omitted. |
ui.entity_colors() |
Draws the category dropdown inline inside a Filters group. Non-Global categories expose Override global; enabled overrides show a color picker and settings popup. Returns false without host support. |
if esp_colors.available() then
esp_colors.set("npc_friendly", {
enabled = true,
color = {0.2, 0.9, 0.4, 1},
second_color = {0.1, 0.5, 0.2, 1},
tint_fill = false
})
ui.window("entity_colors", "Entity colors", function()
ui.group("Filters", function() ui.entity_colors() end)
end)
end
The host supplies Entity kind and relationship; scripts edit persisted appearance only. Writes survive script stop. Category colors affect enabled ESP features after the user enables the master, and preserve independent health colors and host-palette precedence. See entity classification colors for the category mapping and profile migration contract.
Native controls and optional themed widgets
Native controls
local changed, enabled = imgui.checkbox("Enabled", enabled)
imgui.set_next_item_width(140)
changed, amount = imgui.slider_float("Amount", amount, 0, 1)
changed, count = imgui.slider_int("Count", count, 0, 10)
changed, choice = imgui.combo("Mode", choice, {"First", "Second"})
changed, amount = imgui.drag_float("Drag", amount, 0, 1, 0.01)
changed, count = imgui.drag_int("Count", count, 0, 100, 1)
if imgui.radio_button("Mode A", choice == 1) then choice = 1 end
imgui.text_wrapped("A longer description that wraps within the current layout.")
Themed widgets
-- widgets and ui.widgets are the same table. Legacy ui.* names remain compatible.
local changed, enabled = widgets.toggle("Enabled", enabled)
changed, amount = widgets.slider("Amount", amount, 0, 1)
changed, count = widgets.slider_int("Count", count, 0, 10)
changed, choice = widgets.combo("Mode", choice, {"First", "Second"})
changed, selected = widgets.multi_combo("Targets", selected, {"One", "Two"})
if widgets.button("Apply") then print("Applied") end
widgets.columns(2, function()
widgets.group("First", function() widgets.toggle("Example", false) end)
widgets.next_column()
widgets.group("Second", function() imgui.text("Shared theme") end)
end)
Native layout and scoped state
imgui.table and columns
imgui.table("layout", 2, {borders=false, resizable=true, row_bg=false,
width=0, height=0, scroll_y=false}, function()
-- Width is a stretch weight unless fixed=true. Setup precedes any rows.
imgui.table_setup_column("Controls", 1, false)
imgui.table_setup_column("Preview", 1, false)
imgui.table_headers_row()
imgui.table_next_row(24) -- optional minimum row height
imgui.table_next_column()
imgui.text("Column one")
imgui.table_set_column(2) -- one-based
imgui.child("preview", 0, 100, function() imgui.text("Column two") end,
{border=true, horizontal_scroll=true})
end)
Groups, IDs and placement
imgui.with_id("unique_scope", function()
imgui.group(function()
local x,y = imgui.cursor_local()
imgui.set_cursor(x+8,y)
imgui.button("Repeated label")
imgui.same_line(8)
imgui.dummy(12,24)
end)
end)
local screenX,screenY = imgui.cursor()
imgui.set_cursor_screen(screenX,screenY)
local width,height = imgui.available()
local windowX,windowY = imgui.window_pos()
local windowWidth,windowHeight = imgui.window_size()
local textWidth,textHeight = imgui.text_size("Measure me")
imgui.with_font_size
imgui.with_font_size(14, function()
imgui.text("Readable standalone text")
imgui.checkbox("Enabled", true)
end)
Sets the current font size for this scope in physical pixels (8-80), restoring the previous size even when the callback raises an error. The host's font family and glyph coverage are retained.
imgui.with_style_vars
imgui.with_style_vars({WindowPadding={8,8},FramePadding={4,2},ItemSpacing={5,3},
CellPadding={4,3},WindowRounding=3,FrameRounding=0,GrabMinSize=9},function()
imgui.button("Compact")
end)
Supported scalar names: Alpha, DisabledAlpha, WindowRounding, WindowBorderSize, ChildRounding, ChildBorderSize, PopupRounding, PopupBorderSize, FrameRounding, FrameBorderSize, IndentSpacing, ScrollbarSize, ScrollbarRounding, GrabMinSize, GrabRounding, TabRounding. Vector names: WindowPadding, WindowMinSize, WindowTitleAlign, FramePadding, ItemSpacing, ItemInnerSpacing, CellPadding, ButtonTextAlign, SelectableTextAlign. Values are finite 0-256; alpha is 0-1. All scopes restore state even if their callback raises an error. Existing imgui.with_style(colors, callback) accepts the bundled ImGui color names.
Tabs and trees
imgui.tab_bar("pages",function()
imgui.tab_item("Main",function() imgui.text("Main page") end)
imgui.tab_item("Settings",function() imgui.text("Settings page") end)
end)
imgui.tree("Details",function() imgui.text("Expanded details") end)
Custom widgets and drawing
Hit targets and input
local x,y = imgui.cursor()
if imgui.invisible_button("custom_toggle", 120, 24) then enabled = not enabled end
local hovered,active = imgui.is_item_hovered(),imgui.is_item_active()
local x1,y1,x2,y2 = imgui.item_rect()
local mouseX,mouseY = imgui.mouse_pos()
local deltaX,deltaY = imgui.mouse_delta()
local down = imgui.is_mouse_down(0) -- 0 left, 1 right, 2 middle, 3/4 extra
local clicked,released = imgui.is_mouse_clicked(0),imgui.is_mouse_released(0)
local itemClicked = imgui.is_item_clicked(0)
draw.rect(x,y+4,30,16,enabled and {0.5,0.8,0.2,1} or {0.2,0.2,0.2,1},true,8)
draw.circle(x+(enabled and 22 or 8),y+12,5,{1,1,1,1},true)
draw.text(x+40,y+5,"Custom toggle",{1,1,1,1},13)
Window drawing and clipping
-- Screen coordinates. draw.* uses the current window's draw list and clipping.
-- render.* retains the existing foreground overlay behavior.
draw.text(x,y,"Label",{1,1,1,1},13)
draw.line(x,y,x+100,y,{0.5,0.8,0.2,1},2)
draw.rect(x,y,100,30,{0.1,0.1,0.1,1},true,4)
draw.circle(x+12,y+12,8,{1,1,1,1},false,2)
draw.triangle(x,y,x+12,y,x+6,y+8,{1,1,1,1},true,1)
imgui.with_clip_rect(x,y,x+100,y+30,function()
draw.text(x,y,"Clipped content",{1,1,1,1},13)
end)
Custom dropdowns and popups
if imgui.button("Custom dropdown") then imgui.open_popup("choices") end
imgui.popup("choices",function()
if imgui.selectable("First",choice==1) then choice=1 end
if imgui.selectable("Second",choice==2) then choice=2 end
-- selectable(label, selected [, width, height, keep_open])
-- With keep_open=true, Lua can build a multi-select list.
end)
imgui.combo_custom("Custom combo",tostring(choice),function()
if imgui.button("Use first") then choice=1;imgui.close_popup() end
end)
Slim slider appearance
imgui.set_next_item_width(170)
changed, amount = imgui.slider_float("Amount", amount, 0, 1, {style="track"})
changed, count = imgui.slider_int("Count", count, 0, 100, {style="track"})
-- Default / {style="native"}: standard Dear ImGui slider.
-- Track: drag to adjust; click the value / Ctrl-click to type.
-- Arrow adjustment is available when the host enables ImGui keyboard navigation.
-- Track numeric entry clamps to the supplied range. Colors and sizing use ImGui style.
Compact example GUI
-- Run ImGui Demo.lua from Lua > Scripts; F10 toggles visibility.
-- Main: native ImGui widgets aligned in titled panels and responsive columns.
-- Visuals: Lua-drawn toggles, slider and dropdown contents with a live local preview.
-- Settings: accent/footer preferences and optional widgets.* controls.