gui
Menu UI + config writes (the only write path). After creating a container, keep the returned object reference and mount child elements through its methods.
Top-level Creation + Lookup
gui.tab(name) → Tab
Parameter: name : string — used as both id and label.
gui.sub_tab(parent, id, label) → Tab
Parameters: parent : Tab, id : string, label : string
gui.window(id, title, w, h, flags?) → Window
Parameters: id : string, title : string, w : number, h : number, flags? : int = 0
Combine flags from gui.WINDOW.* (bitwise OR / plain +). Common ones:
| Constant | Effect |
|---|---|
NoTitleBar | No title bar |
NoResize | No corner-resize |
NoMove | Not draggable |
NoScrollbar / NoScrollWithMouse | No scrollbar / no wheel scroll |
NoCollapse | Not collapsible |
NoBackground | Transparent (no fill/border) |
AlwaysAutoResize | Size to content |
NoSavedSettings | Don't persist pos/size |
NoMouseInputs / NoInputs | Mouse passthrough / no input at all |
NoFocusOnAppearing / NoBringToFrontOnFocus | Don't steal focus on show/click |
NoDecoration | = NoTitleBar+NoResize+NoScrollbar+NoCollapse (typical borderless HUD) |
Borderless draggable HUD: gui.window("hud", "", 250, 112, gui.WINDOW.NoDecoration + gui.WINDOW.NoFocusOnAppearing). Draw custom bars/graphics inside via Window:custom(id, proto, h) on_render(self,x,y,w,h) (window content-region screen coords, moves with the window). A display-only custom (no mouse hooks) does not capture input, so a borderless window stays draggable by its body.
gui.find(path) → GUIElement | nil
Parameter: path : string — full path (dot-separated, e.g. "aimbot.master").
Returns: a derived-type object (Checkbox / Slider / Combobox / Window / Groupbox / Tab / …). nil means the path does not exist.
gui.children(path) → table<GUIElement>
Parameter: path : string — container full path (Tab / Groupbox / Window / SettingsPopup).
Returns: an array of the container's direct child controls (type-transparent — each supports :get()/:set()/:set_hint()). Returns an empty table {} for non-containers or missing paths. Use it to iterate the controls under a container (e.g. for linked show/hide).
gui.get(path, fallback?) → value | fallback
Parameters: path : string, fallback? : any = nil
Internally = gui.find(path):get() with a nil check.
gui.set(path, value)
Parameters: path : string, value : any
Internally = gui.find(path):set(value).
gui.show(paths) / gui.hide(paths)
Parameter: paths : table<string> — list of control full paths.
gui.set_enabled(paths, state) / gui.set_visible(paths, state)
Parameters: paths : table<string>, state : bool
Main Menu State
gui.is_visible() → bool
Whether the menu is currently open (synchronous query — equivalent to event.on("menu_toggled", fn) but without maintaining your own bool).
gui.list_hotkeys() → table<GUIElement>
Returns every Checkbox + KeybindControl that has an actual key bound (controls with vk = 0 are not included). Order matches the internal insert order, which matches the menu display order.
Returned elements are type-transparent (Checkbox / KeybindControl userdata), so you can call subclass methods directly:
:get_full_path() → string— use withinput.is_active(path)to query current activation:get_label() → string— display label:get() → bool— current checkbox value
Pair with input.format(path) to get a "Ctrl+F1"-style display string.
-- Custom hotkey HUD: list every hotkey-bound control + state + key
for _, elem in ipairs(gui.list_hotkeys()) do
local path = elem:get_full_path()
local key = input.format(path) -- "Ctrl+F1"
local on = input.is_active(path) -- currently active?
log.info((on and "[ON] " or "[off] ") .. elem:get_label() .. " " .. key)
endAligned with fatality's gui.GetHotkeyList().
gui.get_main_window() → table { x, y, w, h, visible }
Screen position, size, and visibility of the main menu ImGui window.
| Field | Description |
|---|---|
x, y | Top-left screen coordinates (pixels, same coord system as draw.*) |
w, h | Current displayed size (live during the fade animation) |
visible | Equivalent to gui.is_visible() |
When the menu is fully closed, x/y/w/h keep their last value and visible=false. The menu is draggable; x/y follow user drags (including across monitors).
-- A docked floating window that follows the menu's right edge
local win = gui.window("dock", "Toolbar", 200, 400)
event.on("frame_update", function()
local m = gui.get_main_window()
if m.visible then
win:set_pos(m.x + m.w + 10, m.y)
win:show()
else
win:hide()
end
end)
-- Draw ESP only when the menu is closed (avoids overlap)
event.on("frame_update", function()
if gui.is_visible() then return end
draw.text(...)
end)Scenario-aware paths (auto link icon)
Any menu control whose full_path contains one of .hidden. / .visible. / .knocked. / .teammate. segments is auto-detected as an ESP scenario field. A link icon (⛓️ linked / ⛓️💥 unlinked) appears to the left of the control; toggling it broadcasts the current value to the other 3 scenario paths. The link state persists in cfg.visual.esp_linked_fields and is saved per plan.
Example:
local tab = gui.tab("MyESP")
local scenarios = {"hidden", "visible", "knocked", "teammate"}
for _, sc in ipairs(scenarios) do
local g = tab:group(sc, sc, 0, 0, 0, 0)
g:color_edit("tint", "Tint", 1, 0, 0, 1)
g:slider_float("radius", "Radius", 1, 20, 5)
end
-- 4 paths "MyESP.<sc>.tint" / "MyESP.<sc>.radius" automatically get a link icon
-- Clicking ⛓️💥 → ⛓️ broadcasts the current scenario's value to the other 3 pathsReading scenario values — two patterns:
-- (A) Inside ESP draw callback: ctx.scenario is per-player (engine-resolved)
esp.add({
id = "my_module",
fn = function(ctx)
local sc_name = ({[0]="hidden",[1]="visible",[2]="knocked",[3]="teammate"})[ctx.scenario]
local tint = gui.get("MyESP." .. sc_name .. ".tint")
return { text = "x", text_color = tint }
end,
})
-- (B) Outside draw context: hardcode which scenario to read
local visible_tint = gui.get("MyESP.visible.tint")There is NO global "current scenario" — the 4 scenarios are per-player state classifications, and N players can be in different scenarios simultaneously. esp.scenario is the menu preview index, not any player's scenario; use ctx.scenario (not esp.scenario) in draw callbacks.
Limitation: Lua-side link propagation only broadcasts at the moment the user toggles unlinked→linked. Subsequent edits to the source value do NOT automatically sync to the other 3 paths (real-time per-frame propagation is TODO). C++ BIND_SCENARIO_FIELD-bound controls don't have this limitation (every write goes through post_write auto-broadcast).
Weapon Configuration Read/Write
Weapon configuration is accessed through gui.set / gui.get. Two path shapes both edit the same underlying WeaponConfig:
| Path shape | Which weapon | When to use |
|---|---|---|
Menu path: aimbot.legit.fov.dead | The weapon currently selected in the menu | Matches UI flow; copy directly from the "show component path" tooltip |
Storage path: aimbot.weapon.<id>.aim.fov_dead | The specific <id> weapon (ignores UI selection) | Batch-edit multiple weapons, run presets, or write while not alive |
Menu tooltip: with "show component path" on, hovering a per-weapon control shows
Storage: aimbot.weapon.<id>.<field>+Menu: aimbot.legit.<...>; right-click copies the storage path. Batch (multi-select) mode adds(batch: N weapons). Global (non-per-weapon) controls show only the single menu path.
Menu paths (recommended for daily use)
gui.set("aimbot.legit.fov.dead", 0.5) -- current selected weapon: dead zone
gui.set("aimbot.legit.fov.hip", 12.0) -- current selected weapon: hip FOV
gui.set("aimbot.legit.smoothadv.kp", 1.8)Whichever weapon is selected in the menu is the one written. default selected → writes to default; r301 selected → writes to r301.
Two caveats:
- Batch mode writes only the first weapon: with multiple weapons multi-selected in the UI, a Lua menu-path
gui.setonly writes to the first one inselected_weapon_ids— unlike dragging the slider in the UI, which truly batches. To batch from Lua, loop over storage paths instead. @weapon_idsuffix forces a specific weapon:aimbot.legit.fov.dead@r301bypasses the UI selection by binding the groupbox tor301for this call. Closer to the UI mental model than looping over storage paths when you want to touch a handful of weapons.
Storage paths (specific weapon)
Format: aimbot.weapon.<weapon_id>.<sub_path>
<weapon_id>: string weapon ID like"r301"/"flatline";"default"is the baseline<sub_path>: WeaponConfig field (dot-nested + 1-based[n]for arrays)
-- Specific weapon: hard-code the ID (unaffected by UI selection)
gui.set("aimbot.weapon.r301.aim.fov_hip", 12.5)
gui.set("aimbot.weapon.flatline.aim.fov_hip", 12.5)
gui.set("aimbot.weapon.default.filter_tags[3]", true)
-- Nested field / array
gui.set("aimbot.weapon.r301.aim.pid.kp", 1.8)
gui.get("aimbot.weapon.r301.aim.max_dist_range[2]")Field names follow the C++ SCHEMA; not enumerated here.
Write failure semantics: all three of these silently no-op — no Lua error, no config snapshot publish:
- Weapon ID not added to armory (e.g.
aimbot.weapon.unknown.aim.fov_hip) — does not silently fall back to default - Unknown field (typo, e.g.
aimbot.weapon.r301.aim.fov_hipx) - Array index out of range / type mismatch (e.g. writing a string to a float field)
Reads (gui.get) the same: any of the above returns nil (or your fallback arg).
gui.get_active_override_path() → string
Returns the storage-path prefix of the config that actually drives the aimbot right now for the held weapon. Semantics match the aim thread's internal resolve_active_weapon:
| Current state | Returns |
|---|---|
| Held weapon is in a group + group is in armory | "aimbot.weapon.<group_id>" |
| Held weapon standalone + in armory | "aimbot.weapon.<weapon_id>" |
| Held weapon not in armory (armory disabled for it) | "aimbot.weapon.default" |
| ClientData snapshot unavailable (DMA disconnected / loading) | "" |
local p = gui.get_active_override_path()
if p ~= "" then
gui.set(p .. ".aim.bone_target", 2) -- writes to the actually-effective config
endThis differs from "menu selected weapon": it looks at the in-game held weapon (lp.weapon_enum resolved via enum_to_weapon_id + armory check), the menu selection doesn't affect it. The empty string is reserved for genuinely-no-state cases (dead, loading) — any alive state returns at least aimbot.weapon.default.
gui.get_selected_weapon() → string
Returns the weapon id currently selected for configuration in the weapon armory (not the held weapon — for that use gui.get_active_override_path()). Use it to address per-weapon Lua values via aimbot.weapon.<wid>.lua.<id>. Returns "default" when nothing is selected.
local wid = gui.get_selected_weapon()
gui.set("aimbot.weapon." .. wid .. ".lua.my_gain", 1.5)Notifications (gui.notify)
gui.notify is a sub-table; call its methods with ::
gui.notify:info(text, dur?) / :success / :warn / :error
Parameters: text : string, dur? : number = 4.0 (lifetime in seconds)
gui.notify:info("hello")
gui.notify:success("Preset applied", 2.5)
gui.notify:warn("Distance too far", 1.5)
gui.notify:error("HTTP failed: " .. err, 3.0)| level | color | use for |
|---|---|---|
| info | white | normal operation feedback |
| success | green | positive confirmation |
| warn | yellow | soft warning |
| error | red | error alert |
Container Methods (shared by Tab / Window / Groupbox)
:group(id, label, x, y, w, h) → Groupbox
Parameters: id : string, label : string, x/y/w/h : number
:button(id, label, callback?) → Button
Parameters: id : string, label : string, callback? : function()
:checkbox(id, label, default) → Checkbox
Parameters: id : string, label : string, default : bool
:slider(id, label, min_v, max_v, def) → Slider (integer)
Parameters: id : string, label : string, min_v / max_v / def : int
:slider_float(id, label, min_v, max_v, def) → SliderFloat
Parameters: id : string, label : string, min_v / max_v / def : number
:range_slider_float(id, label, min_v, max_v, def_v1, def_v2, min_range?) → RangeSliderFloat
Parameters: 5 numbers + min_range? : number = 0
:range_slider_int(id, label, min_v, max_v, def_v1, def_v2, min_range?) → RangeSliderInt
Parameters: 5 ints + min_range? : int = 0
:dropdown(id, label, items, def) → Combobox
Parameters: id : string, label : string, items : table<string>, def : int (1-based default index; the binding layer converts to 0-based internally. gui.get/set reads/writes the internal field directly so that side is 0-based int)
(The usertype is still named Combobox — historical naming, used in the control-methods section below. Multi-select variant is :multi_dropdown.)
:input_text(id, label, def) → InputText
Parameters: id : string, label : string, def : string
:color_edit(id, label, r, g, b, a?) → ColorEdit
Parameters: id : string, label : string, r / g / b : number, a? : number = 1.0
:multi_dropdown(id, label, items, defaults?) → MultiDropdown
Parameters: id : string, label : string, items : table, defaults? : table<bool>
:keybind_control(id, label, def_key?, def_mode?) → KeybindControl
Parameters: id : string, label : string, def_key? : int = 0, def_mode? : int = 0
:live_table(id, headers, provider, height?) (Window / Groupbox only)
Parameters: id : string, headers : table<string>, provider : function() → table<row>, height? : number = 0
row shape: {cell1, cell2, ...} array, or {cells = {...}, color = {r,g,b,a}} table.
:tips(id, text, icon?, color?) (Window / Groupbox / SettingsPopup)
Parameters: id : string, text : string, icon? : string, color? : table<r,g,b,a>
:settings(id, label, icon?) → SettingsPopup
Parameters: id : string, label : string (text left of the gear, may be empty), icon? : string (gear glyph, default ⚙)
A cogwheel/collapsible container (fatality gui.Settings equivalent): shows label ⚙ on the row; clicking the gear opens a popup whose children render inside. See "Settings Gear Container" below.
Custom Controls (LuaControlProto-style)
container:custom(id, proto, height?) → CustomControl
Parameters:
id : string— control idproto : table— control prototype holding theon_*lifecycle callbacks andinitial_dataheight? : number = 24— control height in pixels
Returns a CustomControl userdata. The same proto may instantiate multiple controls; initial_data is deep-copied so instances do not share state.
All proto lifecycle fields are optional:
| Field | Signature | Triggered |
|---|---|---|
initial_data | table | Initial instance data, deep-copied into self.data |
on_render | fn(self, x, y, w, h) | Every frame; the box is (x, y) → (x+w, y+h) in screen coordinates |
on_first_render | fn(self) | First frame (after persisted data has been loaded) |
on_mouse_down | fn(self, btn) | Mouse pressed (0=left, 1=right, 2=middle, 3=x1, 4=x2) while hovered |
on_mouse_up | fn(self, btn) | Mouse released (hover not required) |
on_mouse_move | fn(self, dx, dy) | Hover; mouse position relative to top-left of the control |
on_focus | fn(self) | Edge where the control becomes active (holds mouse) |
on_blur | fn(self) | Edge where active is released |
on_key | fn(self, imgui_key) | Keyboard pressed while hovered (no-repeat) |
on_reset | fn(self) | When self:reset() is called explicitly |
CustomControl instance methods
| Method | Description |
|---|---|
self.data : table | Instance data, read/write. Whole-replace or self.data.foo = x both work |
self:lock_input() | Mark input as locked (ImGui auto-locks when holding mouse; this is a hint) |
self:unlock_input() | Release the lock |
self:is_hovered() → bool | Whether the cursor was over the control last frame |
self:is_focused() → bool | Whether the control held the mouse last frame (active) |
self:is_input_locked() → bool | Query the lock_input flag |
self:get_height() → number / :set_height(h) | Control height |
self:reset() | Trigger on_reset callback |
Persistence
self.data is automatically saved into cfg.lua.<full_path> (same machinery as Lua-created checkboxes). Script reload / plan switch restores the last data. on_first_render fires after data is loaded so you can initialize from persisted state.
-- Counter — left-click +1, right-click -1, count survives reload
local Counter = {
initial_data = { count = 0 },
on_render = function(self, x, y, w, h)
local bg = self:is_hovered() and draw.rgba(0.25, 0.30, 0.40, 1)
or draw.rgba(0.15, 0.18, 0.22, 1)
draw.rect(x, y, x+w, y+h, bg, { filled = true, rounding = 3 })
draw.text(x + 8, y + 8, draw.rgba(1, 1, 1, 1),
"Count: " .. tostring(self.data.count), { outline = true })
end,
on_mouse_down = function(self, btn)
if btn == 0 then self.data.count = self.data.count + 1
elseif btn == 1 then self.data.count = self.data.count - 1 end
end,
}
local grp = gui.tab("misc"):group("g", "demo", 0, 0, 350, 0)
grp:custom("counter1", Counter, 28)Full demo at out/Debug/scripts/custom_control_demo.lua (Counter / Pulse / Bar).
Settings Gear Container (SettingsPopup)
container:settings(id, label, icon?) → SettingsPopup creates a cogwheel/collapsible container (fatality gui.Settings equivalent): the row shows label ⚙; clicking the gear opens a popup whose children render inside. The gear and popup are drawn entirely by the C++ framework — Lua only creates the container and adds children.
Use it to fold "secondary / advanced settings" behind a gear: the main menu stays one row instead of spreading out many params (and you avoid per-frame set_visible juggling).
Child factories (same set as Groupbox; children get persistence / hotkeys / per-weapon writes automatically): button / checkbox / slider / slider_float / dropdown / input_text / color_edit / range_slider_float / range_slider_int / multi_dropdown / keybind_control / tips / custom
Instance methods:
| Method | Description |
|---|---|
s:add_child(elem) / s:remove_child(elem) / s:clear_children() / s:remove_children_by_owner(path) | child add/remove (same as other containers, except remove_child / clear_children return self for chaining) |
s:set_gear_icon(icon) → self | change the gear glyph |
s:set_popup_width(px) → self | popup logical width (default 260) |
local grp = gui.tab("misc"):group("g", "demo", 0, 0, 350, 0)
local adv = grp:settings("adv", "Advanced") -- row shows "Advanced ⚙"
adv:slider("speed", "Speed", 0, 100, 50)
adv:slider_float("ratio", "Ratio", 0.0, 1.0, 0.5):set_format("%.2f")
adv:checkbox("smooth", "Smooth", true)
adv:set_popup_width(220)Container Generic Methods
Shared between Tab / Groupbox / Window for low-level child management. Most scripts don't need these — control creation via :checkbox / :slider / :group is more concise. Use them for dynamic add/remove (hot-reload self-maintenance, conditional control creation at runtime).
:add_child(elem)
Manually attach a GUI element. No return value.
:remove_child(elem)
Remove one direct child. Takes the element object (not an id string). No return value.
:remove_children_by_owner(script_path)
Remove every child created by that owner. No return value. Script unload runs this automatically — manual calls are usually unnecessary.
:clear_children()
Remove every child.
Tab:is_lua_tab() → bool
Whether the tab was created by a Lua script via gui.tab(...) (as opposed to a built-in C++ tab). Use it when you want to avoid touching built-in tabs.
Window-only Methods
Window:is_open() → bool / Window:set_open(bool)
Window:bind_visible(checkbox) → Window
Parameter: checkbox : Checkbox — reference to any Checkbox object (returned by :checkbox(...)).
Each frame auto-syncs Window.open = checkbox.value, saving you from writing set_open inside a live_table provider.
Checkbox / Slider / Combobox Control Methods
Checkbox / Slider / SliderFloat Multi-hotkey
2026-05-25 UX redesign: Each Checkbox / Slider supports multiple hotkeys. Right-click the control to open a popup for managing them. The switch / slider's top-right corner shows the first hotkey plus a count (Ctrl+F1 +2). Each row in the popup has [key][mode][🗑]; Slider rows have an extra [⚙] to set the trigger value (dial slider back to that value).
-- Checkbox multi-hotkey: any active → checkbox = true
local cb = group:checkbox("aim", "Aimbot", false)
cb:add_hotkey({ vk = 0x70, mode = 1 }) -- F1 Hold
cb:add_hotkey({ vk = 0x71, mode = 0, modifier = 0x11 }) -- Ctrl+F2 Toggle
cb:clear_hotkeys() -- clear
-- Slider multi-hotkey + per-key value: F3 writes slider to 50
local sl = group:slider("fov", "FOV", 0, 100, 30)
sl:add_hotkey({ vk = 0x72, mode = 0, value = 50 }) -- F3 Toggle → write 50
sl:add_hotkey({ vk = 0x73, mode = 0, value = 80 }) -- F4 Toggle → write 80Modes: 0=Toggle / 1=Hold / 2=Always. Modifiers: 0x10=Shift / 0x11=Ctrl / 0x12=Alt.
Checkbox:set_keybind(vk, mode, modifier?)
Parameters:
vk : int— Main key virtual-key codemode : int— 0=Toggle / 1=Hold / 2=Alwaysmodifier : int?— Optional modifier key.VK_CONTROL(0x11) /VK_SHIFT(0x10) /VK_MENU(0x12, Alt). Omit or 0 for no modifier.
Example:
cb:set_keybind(0x70, 0) -- F1 alone, Toggle
cb:set_keybind(0x70, 1, 0x11) -- Ctrl + F1, HoldCheckbox:set_colors(table) / Checkbox:get_colors() → table
Custom palette for checkboxes that carry keybind state (current/inactive/hover/...).
:set_hint(text) → self
Available on: Checkbox / Slider / SliderFloat / RangeSliderFloat / RangeSliderInt / InputText / KeybindControl. Combobox / MultiDropdown / ColorEdit / Button do not have :set_hint.
:set_width(px) → self / :get_width() → number
Parameter: px : number — control pixel width; 0 or omitted = auto-fill the row.
A GUIElement base method available on all controls; only Slider / SliderFloat / Combobox honor it (others store but ignore). grp:slider("id","label",0,100,5):set_width(120).
:set_per_weapon(enable?) → self / :is_per_weapon() → bool
Parameter: enable? : bool = true
Store the control's value per weapon (the one currently selected in the weapon armory; Lua controls only). Switching the selected weapon shows that weapon's value; a weapon never set falls back to the default weapon's value. Put it last in the chain (returns the base element).
The value is stored at aimbot.weapon.<wid>.lua.<control id>; scripts can gui.get/set that path to address any specific weapon. Switching weapons shows the selected weapon's value; a weapon never set falls back to the default weapon's value.
Value-type controls only (checkbox / slider / slider_float / combobox / input_text); controls with a keybind or colors are not supported per-weapon. Control ids must be unique within the addressing namespace.
local grp = gui.find("aimbot.legit.smoothadv")
grp:slider_float("lua_gain", "Lua gain", 0, 5, 1):set_format("%.2f"):set_per_weapon(true)Slider:set_format(fmt) → Slider / SliderFloat:set_format / RangeSliderFloat:set_format / RangeSliderInt:set_format
Combobox:on_change(fn) → Combobox
Parameter: fn : function(idx_1based : int, name : string) — 1-based index + selected item string.
Combobox:add_item(item) → Combobox / add_items(items) / remove_item(idx_or_name) / clear_items()
Combobox:get_selected_name() → string / get_items() → table<string>
InputText:set_size(vec2)
Button:set_size(vec2) / set_icon(string)
ColorEdit:set_colors(table) / set_alpha(bool)
MultiDropdown:add_item(item, default?) / add_items(items) / remove_item(idx_or_name) / clear_items() / get_selected_names() → table<string>
GUIElement Base Methods (inherited by all controls)
:get_id() → string / :get_owner() → string / :get_label() → string / :set_label(str)
:is_enabled() → bool / :set_enabled(bool) / :is_visible() → bool / :set_visible(bool)
:get() → value / :set(value) / :has_value() → bool
Path-based unified accessor for the control's current value (equivalent to gui.get / gui.set).
:bind_weapon(...) / :get_target_weapon(...)
Control-level weapon binding (advanced).
Example
local tab = gui.tab("MyTab")
local g = tab:group("g", "My Group", 0, 0, 0, 0)
local cb = g:checkbox("enable", "Enable", false)
:set_hint("Tick to turn the feature on")
local sl = g:slider_float("scale", "Scale", 0.0, 5.0, 1.0)
:set_format("%.2fx")
local cb_theme = g:dropdown("theme", "Theme",
{ "Default", "Dark", "Light" }, 1)
:on_change(function(idx_1based, name)
log.info("Theme switched to: " .. name)
end)
g:button("reset", "Reset", function()
gui.set("MyTab.g.enable", false)
gui.set("MyTab.g.scale", 1.0)
end)