Start here

Download Markdown
First 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 Markdown
CS2 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.

  1. Open an example in Editor or choose New script.
  2. Save with a simple filename, without .lua or a path. Names allow Unicode letters, numbers, spaces, underscores and hyphens, up to 64 UTF-8 bytes.
  3. 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.
  4. 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

hook:remove()

hook:remove() -- idempotent; also done on unload/reload/load failure
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, velocity as vectors; weapon as 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
v:clone() -> vector2
local v=vector2(3,4)
local result=v:clone()
vector2.unpack
v:unpack() -> x,y
local v=vector2(3,4)
local result=v:unpack()
vector2.length
v:length() -> number
local v=vector2(3,4)
local result=v:length()
vector2.length_sqr
v:length_sqr() -> number
local v=vector2(3,4)
local result=v:length_sqr()
vector2.normalized
v:normalized() -> vector2
local v=vector2(3,4)
local result=v:normalized()
vector2.dot
v:dot(other) -> number
local v=vector2(3,4)
local result=v:dot(vector2(2,1))
vector2.distance
v:distance(other) -> number
local v=vector2(3,4)
local result=v:distance(vector2(2,1))
vector2.lerp
v:lerp(other,t) -> vector2
local v=vector2(3,4)
local result=v:lerp(vector2(2,1),.5)
vector2.floor
v:floor() -> vector2
local v=vector2(3,4)
local result=v:floor()
vector2.ceil
v:ceil() -> vector2
local v=vector2(3,4)
local result=v:ceil()
vector2.round
v:round() -> vector2
local v=vector2(3,4)
local result=v:round()
rect.clone
r:clone() -> rectangle
local r=rect(20,20,220,100)
local result=r:clone()
rect.width
r:width() -> number
local r=rect(20,20,220,100)
local result=r:width()
rect.height
r:height() -> number
local r=rect(20,20,220,100)
local result=r:height()
rect.size
r:size() -> vector2
local r=rect(20,20,220,100)
local result=r:size()
rect.center
r:center() -> vector2
local r=rect(20,20,220,100)
local result=r: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
r:expand(amount) -> rectangle
local r=rect(20,20,220,100)
local result=r:expand(4)
rect.shrink
r:shrink(amount) -> rectangle
local r=rect(20,20,220,100)
local result=r:shrink(4)
color.clone
c:clone() -> color
local c=color(.2,.6,1)
local result=c: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.screen_size() -> width,height
local width,height=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.normalize_angle(degrees) -> [-180,180)
print(mathx.normalize_angle(270))
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.info() -> table
print(json.encode(cs2.info()))
cs2.connected
cs2.connected() -> boolean
if cs2.connected() then print(cs2.map_name()) end
cs2.map_name
cs2.map_name() -> string
print(cs2.map_name())
cs2.clock
cs2.clock() -> {curtime,tick,interval,realtime,frame}
local tick = cs2.clock().tick
cs2.view_angles
cs2.view_angles() -> vector or nil
local angles = cs2.view_angles()
entity.get_local_player
entity.get_local_player() -> player or nil
local me = 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.get(full_handle) -> player or nil
local current = entity.get(saved_handle)
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.get(feature_id) -> value
print(settings.get('espEnabled'))
settings.set
settings.set(feature_id, value)
settings.set('espEnabled',true)
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_down(vk_code) -> boolean
local held = input.is_key_down(0x56)
input.is_key_pressed
input.is_key_pressed(vk_code) -> boolean
if input.is_key_pressed(0x56) then print('V') end
input.mouse_position
input.mouse_position() -> x,y
local x,y=input.mouse_position()
input.menu_open
input.menu_open() -> boolean
local editing=input.menu_open()
timers.after
timers.after(seconds, callback) -> id
timers.after(2,function() print('ready') end)
timers.every
timers.every(seconds, callback) -> id
local id=timers.every(1,function() print(cs2.clock().tick) end)
timers.cancel
timers.cancel(id) -> boolean
timers.cancel(timer_id)
storage.read
storage.read(key, fallback=nil, preserve_null=false) -> value
local prefs=storage.read('preferences',{enabled=true})
storage.write
storage.write(key, JSON-compatible value)
storage.write('preferences',{enabled=true})
storage.delete
storage.delete(key)
storage.delete('preferences')
files.read
files.read(name) -> string or nil
local text=files.read('notes')
files.write
files.write(name, text)
files.write('notes','hello')
files.list
files.list() -> names[]
for _,name in ipairs(files.list()) do print(name) end
json.encode
json.encode(value) -> string
print(json.encode({health=100}))
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.theme() -> {accent,text,muted,panel,border}
local colors=render.theme()
render.scale
render.scale() -> number
local scale=render.scale()
console.log
console.log(text)
console.log('Script loaded')
console.clear
console.clear()
console.clear()
script.name
script.name() -> string
print(script.name())
mathx.distance
mathx.distance(x1,y1,z1,x2,y2,z2) -> number
local d=mathx.distance(0,0,0,3,4,0)
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.clamp(value,min,max) -> number
local hp=mathx.clamp(player.health,0,100)
mathx.lerp
mathx.lerp(a,b,t) -> number
local alpha=mathx.lerp(0,1,.5)
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.get(id) -> boolean or nil
print(features.get('espEnabled'))
features.set
features.set(id, boolean)
features.set('espEnabled',true)
features.active
features.active(id) -> boolean
local active=features.active('espEnabled')
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.bind(id, key, mode)
features.bind('espEnabled','F7','toggle')
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.get(id) -> boolean or nil
print(base.get('espEnabled'))
base.set
base.set(id, boolean)
base.set('espEnabled',true)
base.log
base.log(...)
base.log('Ready',CS2_API_VERSION)
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.off(token)
events.off(token)
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.reset_theme()
ui.reset_theme()
ui.tr
ui.tr(text) -> text
imgui.text(ui.tr('Ready'))
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.feature_visible(feature_id,visible)
ui.feature_visible('espEnabled',false)
ui.menu_visible
ui.menu_visible(visible?) -> boolean
local open=ui.menu_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.entity_colors() -> false
local available=ui.entity_colors()
ui.group
ui.group(label,callback)
ui.group('Settings',function() imgui.text('Content') end)
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.next_column()
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.toggle(label,value) -> changed,value
local changed,value=ui.toggle('Enabled',true)
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(text)
imgui.text('Hello')
imgui.text_colored
imgui.text_colored(text,rgba)
imgui.text_colored('Hello',{.3,.7,1,1})
imgui.button
imgui.button(label,width=0,height=0) -> clicked
local clicked=imgui.button('Apply')
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.same_line(spacing=-1)
imgui.same_line()
imgui.separator
imgui.separator()
imgui.separator()
imgui.spacing
imgui.spacing(height=4)
imgui.spacing(8)
imgui.tooltip
imgui.tooltip(text)
imgui.tooltip('Details')
imgui.progress
imgui.progress(fraction)
imgui.progress(.75)
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.available() -> width,height
local w,h=imgui.available()
imgui.cursor
imgui.cursor() -> screen_x,screen_y
local x,y=imgui.cursor()
imgui.set_cursor
imgui.set_cursor(local_x,local_y)
imgui.set_cursor(20,20)
imgui.is_item_hovered
imgui.is_item_hovered() -> boolean
if imgui.is_item_hovered() then print('hovered') end
render.text
render.text(x,y,text,rgba,size=13)
render.text(20,20,'CS2',{1,1,1,1},14)
render.line
render.line(x1,y1,x2,y2,rgba,thickness=1)
render.line(20,20,120,20,{1,1,1,1},2)
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.time() -> seconds since ImGui start
print(engine.time())
engine.delta_time
engine.delta_time() -> frame seconds
print(engine.delta_time())
engine.fps
engine.fps() -> ImGui average FPS
print(engine.fps())
engine.viewport
engine.viewport() -> width,height in pixels
print(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
v:clone()
local v=vector(3,4,0)
print(v:clone())
vector:unpack
v:unpack()
local v=vector(3,4,0)
print(v:unpack())
vector:length
v:length()
local v=vector(3,4,0)
print(v:length())
vector:length_sqr
v:length_sqr()
local v=vector(3,4,0)
print(v:length_sqr())
vector:length2d
v:length2d()
local v=vector(3,4,0)
print(v:length2d())
vector:normalized
v:normalized()
local v=vector(3,4,0)
print(v:normalized())
vector:dot
v:dot(other)
local v=vector(3,4,0)
print(v:dot(vector(1,0,0)))
vector:cross
v:cross(other)
local v=vector(3,4,0)
print(v:cross(vector(1,0,0)))
vector:distance
v:distance(other)
local v=vector(3,4,0)
print(v:distance(vector(1,0,0)))
vector:lerp
v:lerp(other,t)
local v=vector(3,4,0)
print(v:lerp(vector(1,0,0),.5))
color
color(r=1,g=1,b=1,a=1) -> RGBA table
local accent=color(.3,.6,.8,1)
color:unpack
c:unpack()
print(color(.3,.6,.8):unpack())
color:alpha
c:alpha(a)
local translucent=color(.3,.6,.8):alpha(.5)
color:lerp
c:lerp(other,t)
local mixed=color(0,0,0):lerp(color(1,1,1),.5)
color:to_hex
c:to_hex()
print(color(1,.5,0):to_hex())
player:get_name
p:get_name()
local p=entity.get_local_player()
if p then print(p:get_name()) end
player:get_origin
p:get_origin()
local p=entity.get_local_player()
if p then print(p:get_origin()) end
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
p:get_health()
local p=entity.get_local_player()
if p then print(p:get_health()) end
player:get_armor
p:get_armor()
local p=entity.get_local_player()
if p then print(p:get_armor()) end
player:is_alive
p:is_alive()
local p=entity.get_local_player()
if p then print(p:is_alive()) end
player:is_enemy
p:is_enemy()
local p=entity.get_local_player()
if p then print(p:is_enemy()) end
player:get_weapon
p:get_weapon()
local p=entity.get_local_player()
if p then print(p:get_weapon()) end
player:get_bone
p:get_bone(index)
local p=entity.get_local_player()
if p then print(p:get_bone(6)) end
player:is_valid
p:is_valid()
local p=entity.get_local_player()
if p then print(p:is_valid()) end
player:refresh
p:refresh()
local p=entity.get_local_player()
if p then print(p:refresh()) end
esp_colors.available
esp_colors.available() -> false
print(esp_colors.available())
Shared UI API 1.1 synchronization

imgui.* provides native ImGui controls, scoped layouts, tables, font/style changes, input/hit targets and custom popup contents. draw.* draws in the current window. widgets.* and ui.widgets.* explicitly select CS2's existing widget adapter; legacy ui.* calls remain available. Native sliders optionally accept {style="track"} for slim tracks and editable values. See the canonical complete reference. The in-product API search includes all shared code examples.

The third_party/simple_base/src/ui/lua_ui.* files forward to canonical bindings. CS2 keeps its Lua runtime extensions, game APIs, events, console, storage, page-override rules and ImGui 1.91.8. MSBuild runs tools/sync_lua_api.py before compilation to embed the current docs/examples under build/generated/shared_lua. Rebuild and restart the host to activate updates. No background synchronization service, user-script overwrite or automatic deployment is involved.

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
cs2.capabilities() -> table
print(json.encode(cs2.capabilities()))
utils.base64_encode
utils.base64_encode(bytes) -> string
print(utils.base64_encode("hello"))
utils.base64_decode
utils.base64_decode(text) -> bytes
print(utils.base64_decode("aGVsbG8="))
utils.hex_encode
utils.hex_encode(bytes) -> lowercase hex
print(utils.hex_encode("hello"))
utils.hex_decode
utils.hex_decode(hex) -> bytes
print(utils.hex_decode("68656c6c6f"))
utils.to_bytes
utils.to_bytes(string) -> byte[]
local bytes=utils.to_bytes("hello")
utils.from_bytes
utils.from_bytes(byte[]) -> string
print(utils.from_bytes({72,105,0}))
utils.fnv1a
utils.fnv1a(bytes) -> unsigned 32-bit integer
print(utils.fnv1a("hello"))
utils.unix_time
utils.unix_time() -> integer seconds since Unix epoch
print(utils.unix_time())
mathx.approach
mathx.approach(current,target,max_step) -> number
print(mathx.approach(0,10,2))
mathx.approach_angle
mathx.approach_angle(current,target,max_degrees) -> [-180,180)
print(mathx.approach_angle(170,-170,5))
mathx.lerp_angle
mathx.lerp_angle(current,target,t) -> [-180,180)
print(mathx.lerp_angle(170,-170,.5))
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.smoothstep(t) -> [0,1]
print(mathx.smoothstep(.5))
mathx.smootherstep
mathx.smootherstep(t) -> [0,1]
print(mathx.smootherstep(.5))
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.ease(curve,t) -> [0,1]
local progress=mathx.ease("in_out_cubic",.5)
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.exists(name) -> boolean
print(files.exists("notes"))
files.delete
files.delete(name) -> boolean,error?
local removed,err=files.delete("old notes")
json.array
json.array(table={}) -> tagged table
print(json.encode(json.array()))
json.object
json.object(table={}) -> tagged table
print(json.encode(json.object()))
console.warn
console.warn(message)
console.warn("Optional data is unavailable")
console.error
console.error(message)
console.error("Could not load custom preferences")
console.trace
console.trace(message)
console.trace("Reached update handler")
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
cs2.permissions() -> table
print(cs2.permissions().http)
fs.read
fs.read(path) -> string or nil,error
local data,err=fs.read('profiles/default.json')
fs.write
fs.write(path,bytes) -> true or nil,error
assert(fs.write('profiles/default.json',json.encode({enabled=true})))
fs.mkdir
fs.mkdir(path) -> true or nil,error
assert(fs.mkdir('assets/icons'))
fs.exists
fs.exists(path) -> boolean or nil,error
local exists,err=fs.exists('assets/icons')
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.remove(path) -> boolean or nil,error
assert(fs.remove('profiles/unused.json'))
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.available() -> boolean
print(commands.available()) -- permission state
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.get(id) -> item or nil,error
local item,err=inventory.get(saved_id)
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.remove(id) -> true or nil,error
assert(inventory.remove(saved_id))
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
model_preview.status() -> {state,message}
print(model_preview.status().message)
HttpRequest.status
request:status() -> pending|complete|canceled
print(request: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
request:cancel()
request:cancel()
CommandCallback.enable
callback:enable(boolean)
callback:enable(false)
CommandCallback.remove
callback:remove()
callback:remove()
CommandCallback.get
callback:get(key) -> scalar or nil
print(callback:get('target'))
CommandCallback.set
callback:set(key,scalar_or_nil)
callback:set('strength',1.5)
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 Markdown
CS2 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.get(id)

id string; Returns: boolean?

base.log
base.log(...)

...: any

base.set
base.set(id,value)

id string; value boolean

color
color(r,g,b,a)

r? number; g? number; b? number; a? number; Returns: CS2Color

colors.from_hex
colors.from_hex(hex)

hex string; Returns: CS2Color

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
colors.to_hsv(rgba)

rgba CS2Color|number[]; Returns: number,number,number,number

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.clear()
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.info() -> table

Returns: table

print(json.encode(cs2.info()))
cs2.map_name
cs2.map_name() -> string

Returns: string

print(cs2.map_name())
cs2.permissions
cs2.permissions() -> table

Returns: LuaPermissions

print(cs2.permissions().http)
cs2.view_angles
cs2.view_angles() -> vector or nil

Returns: CS2Vector?

local angles = 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.delta_time()

Returns: number

engine.fps
engine.fps()

Returns: number

engine.time
engine.time()

Returns: number

engine.viewport
engine.viewport()

Returns: number, number

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.off(token)

token integer

events.on
events.on(event,callback)

event CS2Event; callback fun(current?:CS2Player,previous?:CS2Player); Returns: integer token

features.active
features.active(id)

id string; Returns: boolean?

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.bind(id,key,mode)

id string; key string; mode? string

features.color
features.color(id,r,g,b,a)

id string; r number; g number; b number; a? number

features.get
features.get(id)

id string; Returns: boolean?

features.list
features.list()

Returns: table[]

features.set
features.set(id,value)

id string; value boolean

features.trigger
features.trigger(id)

id string

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.available()

Returns: number, number

imgui.button
imgui.button(label,width,height)

label string; width? number; height? number; Returns: boolean

imgui.checkbox
imgui.checkbox(label,value)

label string; value boolean; Returns: boolean, boolean

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()

Returns: number, number

imgui.cursor_local
imgui.cursor_local()

Returns: number, number

imgui.disabled
imgui.disabled(disabled,draw)

disabled boolean; draw function

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.dummy(width,height)

width number; height number

imgui.group
imgui.group(draw)

draw function

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_active()

Returns: boolean

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_item_hovered()

Returns: boolean

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.item_rect()

Returns: number, number, number, number

imgui.mouse_delta
imgui.mouse_delta()

Returns: number, number

imgui.mouse_pos
imgui.mouse_pos()

Returns: number, number

imgui.open_popup
imgui.open_popup(id)

id string

imgui.popup
imgui.popup(label,draw)

label string; draw function; Returns: boolean

imgui.progress
imgui.progress(fraction)

fraction number

imgui.radio_button
imgui.radio_button(label,selected)

label string; selected boolean; Returns: boolean

imgui.same_line
imgui.same_line(spacing,local_x)

spacing? number; local_x? number

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(x,y)

x number; y number

imgui.set_cursor_screen
imgui.set_cursor_screen(x,y)

x number; y number

imgui.set_next_item_width
imgui.set_next_item_width(width)

width number

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.spacing(height)

height? number

imgui.tab_bar
imgui.tab_bar(label,draw)

label string; draw function; Returns: boolean

imgui.tab_item
imgui.tab_item(label,draw)

label string; draw function; Returns: boolean

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_column()

Returns: boolean

imgui.table_next_row
imgui.table_next_row(height)

height? number

imgui.table_set_column
imgui.table_set_column(index)

index integer; Returns: boolean

One-based 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(text)

text string

imgui.text_colored
imgui.text_colored(text,rgba)

text string; rgba CS2Color|number[]

imgui.text_size
imgui.text_size(text)

text string; Returns: number, number

imgui.text_wrapped
imgui.text_wrapped(text)

text string

imgui.tooltip
imgui.tooltip(text)

text string

imgui.tree
imgui.tree(label,draw)

label string; draw function; Returns: boolean

imgui.window
imgui.window(title,options,draw)

title string; options table; draw function

imgui.window_pos
imgui.window_pos()

Returns: number, number

imgui.window_size
imgui.window_size()

Returns: number, number

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_font_size(size,draw)

size number; draw function

imgui.with_id
imgui.with_id(id,draw)

id string; draw function

imgui.with_style
imgui.with_style(colors,draw)

colors table<string,CS2Color|number[]>; draw function

imgui.with_style_vars
imgui.with_style_vars(vars,draw)

vars table; draw function

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.menu_open
input.menu_open() -> boolean

Returns: boolean

local editing=input.menu_open()
input.mouse_position
input.mouse_position() -> x,y

Returns: number, number

local x,y=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.smootherstep(t) -> [0,1]

t number; Returns: number

print(mathx.smootherstep(.5))
mathx.smoothstep
mathx.smoothstep(t) -> [0,1]

t number; Returns: number

print(mathx.smoothstep(.5))
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.export(module,name)

module string; name string; Returns: integer?,string?

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.module(module)

module string; Returns: {base:integer,size:integer}?,string?

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?

print
print(...)

...: any

rect
rect(x0,y0,x1,y1)

x0? number; y0? number; x1? number; y1? number; Returns: CS2Rect

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.scale() -> number

Returns: number

local scale=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
script.name() -> string

Returns: string

print(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.delete(key)

key string

storage.delete('preferences')
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.cancel(id) -> boolean

id integer; Returns: boolean

timers.cancel(timer_id)
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.columns(count,draw,compact)

count integer; draw function; compact? boolean

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(id)

id string

ui.feature_visible
ui.feature_visible(id,visible)

id string; visible boolean

ui.group
ui.group(label,draw)

label string; draw function

ui.keybind
ui.keybind(id,label)

id string; label? string

ui.menu_visible
ui.menu_visible(visible)

visible? boolean; Returns: boolean

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.overlay(id,draw)

id string; draw function; Returns: string

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.theme(options)

options table

ui.toggle
ui.toggle(label,value)

label string; value boolean; Returns: boolean, boolean

ui.tr
ui.tr(english_key)

english_key string; Returns: string

ui.window
ui.window(id,title,options,draw)

id string; title string; options table; draw function; Returns: string

ui.window_visible
ui.window_visible(id,visible)

id string; visible? boolean; Returns: boolean

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
vector(x,y,z)

x? number; y? number; z? number; Returns: CS2Vector

vector2
vector2(x,y)

x? number; y? number Defaults to x.; Returns: CS2Vector2

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.group(label,draw)

label string; draw function

Alias: ui.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.next_column()

Alias: ui.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 Markdown
CS2 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
  1. Replace external module names and retained GUI objects with documented Scooby bindings and UI callback scopes.
  2. Replace assumed feature IDs with values returned by settings.list().
  3. Check optional player/projection results for nil and preserve the full player handle.
  4. Replace native-event assumptions with explicitly sampled behavior only when that behavior is sufficient.
  5. 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 Markdown
Lua 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
  1. Open Lua > Scripts, select Custom UI.lua, and click Run.
  2. A My Tools tab appears in the sidebar. Its controls edit real registered features.
  3. F8 toggles its feature and the example overlay.
  4. Run Standalone Menu.lua to try an independent menu; F9 toggles its visibility.
  5. 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 nil while 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") or error({message="description"}) for useful text. Error reporting does not invoke custom __tostring functions.

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.

Independent menus and attached windows
local window = ui.window("inspector", "My Inspector", {
    width = 360, height = 280,
    menu_only = false,
    attach = "none"
}, function()
    imgui.text("Independent menu")
    if imgui.button("Show main menu") then ui.menu_visible(true) end
end)

ui.window(id, title, options, draw) registers a persistent window. Width/height are initial physical-pixel dimensions. A free window can be dragged, resized and closed. menu_only=true hides it when the main GUI closes.

attach accepts "none", "left" or "right". Attached windows follow the main menu and match its height; their position is clamped to the viewport, so they may overlap the main menu when the requested side has insufficient room. The built-in API reader additionally reserves space beside the menu on normal-sized viewports.

ui.window_visible(windowId [, boolean]) gets/sets the visibility of a window owned by the calling script. This can reopen a window after its close button is used.

ui.menu_visible([boolean]) gets/sets the main GUI's visibility. Independent windows continue drawing when the main GUI is hidden unless menu_only is set.

To toggle a standalone window with a hotkey, register a toggle feature and call ui.window_visible(windowId, features.active(featureId)) from an update callback. See Standalone Menu.lua.

For windows constructed dynamically inside a UI/overlay callback, use imgui.window(title, options, draw). Its options include width, height, initial x/y, no_title_bar, no_resize, no_move, no_scrollbar and auto_resize. It is drawn whenever that scope is called; visibility is controlled by the Lua code surrounding the call.

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/radar
  • lua/scripts, lua/editor, lua/console
  • settings/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 /EHs with /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.
Shared source and automatic updates

V1 and V2 compile the same UI/src/ui/lua_ui.cpp and header. Edit this reference in UI/docs/LUA_API.md and examples in UI/assets/scripts/. Both builds automatically embed and stage these resources. Rebuild and restart the preview or host to receive an update; running binaries do not hot-reload C++ bindings. User-edited data scripts remain unchanged.