Kernal Lua API
Complete reference for the kernal global table available to all scripts. Build overlays, read and write process memory, handle input, and automate game analysis — all from Lua.
kernal.* functions are synchronous from your script's perspective. Memory reads, process operations, and overlay commands are internally asynchronous but automatically awaited — you write normal sequential code and it just works.
Logging & Debug
Three log levels let you categorize output in the script console. All accept variadic arguments which are concatenated with spaces, similar to print() in standard Lua.
kernal.log(...)
Print an info-level message. Use for general status updates and data inspection.
kernal.log("Hello", "world", 123)
-- Output: Hello world 123
-- Useful for debugging values:
kernal.log("Player HP:", hp, "Team:", team)
kernal.warn(...)
Print a warning. Appears highlighted in the console. Use for non-fatal issues.
kernal.warn("Health is low:", hp)
kernal.warn("Entity pointer is NULL, skipping")
kernal.error(...)
Print an error message. Use for failures that don't necessarily crash the script.
kernal.error("Failed to read memory at", addr)
kernal.error("Module not found: engine2.dll")
Timing
Control script execution speed and measure performance. Every main loop should include a sleep() or sync() call to prevent 100% CPU usage.
kernal.sleep(ms)
Pause execution for the specified number of milliseconds. This yields the thread, allowing the engine to process other tasks.
| Param | Type | Description |
|---|---|---|
ms | number | Milliseconds to sleep (0 = yield without delay) |
kernal.sleep(16) -- ~60 FPS tick rate
kernal.sleep(1) -- maximum speed (~1000 FPS)
kernal.sleep(100) -- 10 updates/sec (low CPU usage)
kernal.now()
Returns the current timestamp in milliseconds (Unix epoch). Use for measuring elapsed time between operations.
Returns:number
local start = kernal.now()
-- ... perform expensive work ...
local elapsed = kernal.now() - start
kernal.log("Frame took " .. elapsed .. "ms")
-- Example: refresh entity list every 5 seconds
if kernal.now() - lastRefresh > 5000 then
entity_list = kernal.read_i64(base + offsets.dwEntityList)
lastRefresh = kernal.now()
end
kernal.time_ms() NEW
Returns a high-resolution monotonic timestamp in milliseconds. Ideal for frame timing and delta calculations — not affected by clock drift.
Returns:number (ms)
local frameStart = kernal.time_ms()
-- ... frame work ...
local elapsed = kernal.time_ms() - frameStart
local remaining = 6.9 - elapsed -- cap to ~144 FPS
if remaining > 0 then kernal.sleep(remaining) end
Process Management
Before reading or writing memory, you must attach to a target process. Kernal supports attaching by process name (searches for a running process) or by PID directly.
kernal.attach(pidOrName)
Attach to a process. This opens a handle and prepares memory access. Must be called before any read_* or write_* functions.
| Param | Type | Description |
|---|---|---|
pidOrName | number | string | Process ID or executable name (e.g. "game.exe") |
number (PID of attached process)
-- Attach by name (most common)
kernal.attach("game.exe")
-- Attach by PID (useful if multiple instances)
kernal.attach(12345)
-- Typical startup pattern:
kernal.log("Waiting for game...")
kernal.attach("game.exe")
kernal.log("Attached to PID: " .. kernal.get_pid())
kernal.detach()
Detach from the current process. Called automatically when your script ends, but you can call it manually to cleanly release the handle.
kernal.detach()
kernal.log("Disconnected")
kernal.process_list()
Returns a list of all running processes on the system. Each entry has pid, name, and exe fields.
table of {pid, name, exe}
local procs = kernal.process_list()
for _, p in ipairs(procs) do
if p.name == "game.exe" then
kernal.log("Found at PID " .. p.pid)
break
end
end
-- Launcher detection example:
local launcher = "unknown"
local ok, procs = pcall(kernal.process_list)
if ok and procs then
for _, p in ipairs(procs) do
local name = (type(p) == "table" and p.name or tostring(p)):lower()
if name:find("eadesktop") or name:find("origin") then
launcher = "EA"; break
elseif name:find("steam") then
launcher = "Steam"; break
end
end
end
kernal.log("Launcher: " .. launcher)
kernal.is_attached() NEW
Check if the script is currently attached to a process. Useful for connection-state checks in your main loop.
Returns:boolean
if not kernal.is_attached() then
kernal.error("Lost connection to process!")
return
end
kernal.get_pid() NEW
Get the PID of the currently attached process. Returns 0 if not attached.
Returns:number
kernal.log("Working with PID: " .. kernal.get_pid())
Memory — Reading
Read values from the attached process's memory space. All read functions require kernal.attach() to be called first. If the address is invalid or the process has exited, these functions will throw an error (catch with pcall).
kernal.read(addr, len)
Read raw bytes and return them as a hex-encoded string. Useful for dumping unknown memory regions.
| Param | Type | Description |
|---|---|---|
addr | number | Memory address to read from |
len | number | Number of bytes to read |
string (hex-encoded)
local hex = kernal.read(base + 0x100, 16)
kernal.log(hex) -- "48894c240848895424105355..."
kernal.read_i32(addr)
Read a signed 32-bit integer (4 bytes). Most common for health, ammo, team numbers, etc.
Returns:number
local health = kernal.read_i32(pawn + 0x34C)
local team = kernal.read_i32(pawn + 0x3EB)
kernal.log("HP:", health, "Team:", team)
kernal.read_u32(addr)
Read an unsigned 32-bit integer. Used for handles, flags, and bitmasks that shouldn't be sign-extended.
Returns:number
local handle = kernal.read_u32(controller + 0x90C)
if handle == 0xFFFFFFFF then
kernal.warn("Invalid handle")
end
kernal.read_i64(addr)
Read a signed 64-bit integer. Essential for reading pointers on 64-bit processes. Returned as a Lua number (safe up to 2^53).
Returns:number
-- Read a pointer (64-bit address)
local entityList = kernal.read_i64(base + 0x24D0DC0)
kernal.log(string.format("Entity list @ 0x%X", entityList))
kernal.read_f32(addr)
Read a 32-bit floating-point value. Used for positions, velocities, angles, and any decimal game data.
Returns:number
-- Read player position (vec3 = 3 consecutive floats)
local x = kernal.read_f32(pawn + 0x1390)
local y = kernal.read_f32(pawn + 0x1394)
local z = kernal.read_f32(pawn + 0x1398)
kernal.log(string.format("Pos: %.1f, %.1f, %.1f", x, y, z))
Buffers & Batching
Individual memory reads are fast (~1–2 ms each), but in a main loop reading 50+ values per frame, that latency adds up. Buffers solve this by reading large chunks in a single IPC call, then letting you extract values locally with zero additional cost.
kernal.read_buf(addr, len)
Read a contiguous block of memory into a buffer object. This is the primary optimization tool — use it whenever you need multiple values from the same memory region.
| Param | Type | Description |
|---|---|---|
addr | number | Start address |
len | number | Number of bytes to read |
Buffer object
Buffer methods:
| Method | Description |
|---|---|
.i32(offset) | Signed 32-bit int at byte offset |
.u32(offset) | Unsigned 32-bit int at byte offset |
.i64(offset) | Signed 64-bit int at byte offset |
.f32(offset) | 32-bit float at byte offset |
.str(offset, maxLen) | Null-terminated UTF-8 string starting at offset (default maxLen=256) |
.len() | Total buffer size in bytes |
-- Read a 4x4 view matrix (16 floats = 64 bytes) in ONE call
local buf = kernal.read_buf(base + 0x2330AE0, 64)
local vm = {}
for i = 0, 15 do
vm[i] = buf.f32(i * 4) -- extract each float at its byte offset
end
-- Read player position (3 floats = 12 bytes)
local pos = kernal.read_buf(pawn + 0x1390, 12)
local x, y, z = pos.f32(0), pos.f32(4), pos.f32(8)
kernal.read_multi(addr1, len1, addr2, len2, ...)
Scatter-gather read — reads multiple non-contiguous memory regions in a single IPC round-trip. This is the fastest way to read data from many different addresses at once.
Pass flat pairs of (address, length). Results are packed sequentially in the returned buffer: region 1 starts at offset 0, region 2 at offset len1, region 3 at offset len1+len2, etc.
| Param | Type | Description |
|---|---|---|
addr1 | number | First address to read |
len1 | number | Bytes to read from addr1 |
... | ... | More (address, length) pairs |
Buffer object
-- Read view matrix (64B) + local pawn pointer (8B) in ONE call
local buf = kernal.read_multi(
base + 0x2330AE0, 64, -- view matrix at offset 0
base + 0x2056700, 8 -- local pawn at offset 64
)
local m00 = buf.f32(0) -- first matrix element
local pawn = buf.i64(64) -- pawn pointer
-- Dynamic: batch-read health from N players
local args = {}
for _, pawn in ipairs(pawnAddrs) do
args[#args + 1] = pawn + 0x34C -- health offset
args[#args + 1] = 4 -- 4 bytes (i32)
end
local buf = kernal.read_multi(table.unpack(args))
for i = 1, #pawnAddrs do
local hp = buf.i32((i - 1) * 4)
kernal.log("Player " .. i .. " HP: " .. hp)
end
Memory — Writing
Write values into the target process's memory. Use with caution — writing incorrect values can crash the target. All write functions require an active attach().
kernal.write(addr, hex)
Write raw bytes from a hex string. Commonly used for patching instructions (e.g. NOPs).
| Param | Type | Description |
|---|---|---|
addr | number | Target address |
hex | string | Hex-encoded bytes |
-- NOP out a 4-byte instruction
kernal.write(addr, "90909090")
-- Patch a JNE to JMP (force branch)
kernal.write(jumpAddr, "EB")
kernal.write_i32(addr, value)
Write a signed 32-bit integer.
kernal.write_i32(pawn + 0x34C, 100) -- set health to 100
kernal.write_f32(addr, value) NEW
Write a 32-bit floating-point value.
| Param | Type | Description |
|---|---|---|
addr | number | Target address |
value | number | Float value to write |
kernal.write_f32(pawn + 0x1390, 0.0) -- zero out X position
kernal.write_f32(viewAngle, 90.0) -- set view angle
kernal.write_i64(addr, value) NEW
Write a 64-bit integer. Useful for writing pointers or large values.
| Param | Type | Description |
|---|---|---|
addr | number | Target address |
value | number | 64-bit integer value |
kernal.write_i64(pointerSlot, newAddress)
Memory — Quality of Life
Convenience wrappers that simplify common memory reading patterns. These save you from manual offset math and type conversions.
kernal.read_string(addr, maxLen) NEW
Read a null-terminated UTF-8 string from memory.
| Param | Type | Default | Description |
|---|---|---|---|
addr | number | — | Memory address |
maxLen | number | 256 | Maximum bytes to read (capped at 4096) |
string
local name = kernal.read_string(nameAddr, 64)
kernal.log("Player name: " .. name)
kernal.read_vec3(addr) NEW
Read 3 consecutive floats (12 bytes) as a vector. Shorthand for reading X, Y, Z coordinates.
Returns:table with x, y, z fields
local pos = kernal.read_vec3(pawn + 0x1390)
kernal.log(string.format("Pos: %.1f, %.1f, %.1f", pos.x, pos.y, pos.z))
kernal.read_bool(addr) NEW
Read a single byte and return as boolean.
Returns:boolean
local isDormant = kernal.read_bool(pawn + 0xEF)
kernal.read_u64(addr) NEW
Read an unsigned 64-bit integer (safe up to 2^53).
Returns:number
local ptr = kernal.read_u64(base + 0x100)
kernal.is_valid_ptr(addr) NEW
Check if an address points to a valid mapped memory region. Uses VirtualQueryEx — faster than wrapping every read in pcall.
boolean
if kernal.is_valid_ptr(pawnAddr) then
local hp = kernal.read_i32(pawnAddr + 0x34C)
end
Pointer Chains & Batch Operations
Advanced functions that reduce IPC round-trips by performing multi-step operations server-side. These are critical for high-performance scripts that follow pointer chains or write to multiple locations.
kernal.read_chain(addr, offsets, finalSize) NEW
Follow a pointer chain server-side in one IPC call. Eliminates multi-step pointer resolution. At each step: read 8 bytes (pointer), add the next offset. After all offsets, reads finalSize bytes at the final address.
| Param | Type | Description |
|---|---|---|
addr | number | Starting address |
offsets | table | List of offsets {off1, off2, ...} |
finalSize | number | Bytes to read at the resolved final address |
Buffer (same as read_buf) + .addr() for the resolved final address
-- Before: 3 separate IPC calls
local entryPtr = kernal.read_i64(entityList + 0x10)
local pawnChunk = kernal.read_i64(entryPtr + chunkIdx * 8)
local pawn = kernal.read_i64(pawnChunk + slotIdx * 0x70)
-- After: 1 IPC call
local buf = kernal.read_chain(entityList, {0x10, chunkIdx * 8, slotIdx * 0x70}, 8)
local pawn = buf.i64(0)
kernal.write_multi(addr1, hex1, addr2, hex2, ...) NEW
Batch-write multiple memory locations in one IPC call.
| Param | Type | Description |
|---|---|---|
addr1 | number | First address |
hex1 | string | Hex bytes for first write |
... | ... | More addr/hex pairs |
{written: count}
kernal.write_multi(
addr1, "90909090",
addr2, "01000000"
)
kernal.module_base_multi(name1, name2, ...) NEW
Resolve multiple module bases in one call. Enumerates modules only once.
Returns:table (0-indexed) of {base, size} or null for each name
local mods = kernal.module_base_multi("client.dll", "engine2.dll", "schemasystem.dll")
local clientBase = mods[0].base
local engineBase = mods[1].base
Modules
Get information about loaded DLLs and modules in the target process. Module base addresses are the foundation for offset-based memory access — all game structures live at moduleBase + offset.
kernal.module_base(name)
Get the base address of a loaded module. This is the starting address where the DLL is mapped in memory.
| Param | Type | Description |
|---|---|---|
name | string | Module filename (e.g. "client.dll", "engine2.dll") |
number (base address)
local base = kernal.module_base("client.dll")
kernal.log(string.format("client.dll @ 0x%X", base))
-- Now you can read game data using base + offset:
local entityList = kernal.read_i64(base + 0x24D0DC0)
kernal.module_size()
Get the size (in bytes) of the last module looked up with module_base(). Useful for bounds checking or scanning.
number (bytes)
local base = kernal.module_base("client.dll")
local size = kernal.module_size()
kernal.log(string.format("client.dll: 0x%X — %d MB", base, size / 1024 / 1024))
Overlay — Lifecycle
The overlay is a transparent, always-on-top, click-through window that renders over the game. You control it with a simple open/begin/draw/end/close pattern.
kernal.overlay_open()
Create and display the overlay window. Call once at script startup before entering your main loop.
kernal.overlay_open()
kernal.log("Overlay active")
kernal.overlay_begin()
Begin a new frame. Clears all draw commands from the previous frame. Call at the top of each loop iteration before any draw_* calls.
kernal.overlay_end()
Flush all draw commands and render the frame to screen. Call at the end of each loop iteration after all draw_* calls.
Note: This is fire-and-forget — it queues the frame for rendering but doesn't wait for it to be painted. On high-FPS hardware (300+), the script can outrun the renderer, causing increasing overlay latency. Use flush() or sync() after this call to prevent latency buildup.
kernal.overlay_close()
Destroy the overlay window. Called automatically when your script stops, but you can call it manually.
kernal.flush() NEW
Block the script until the renderer confirms it painted the current frame. Prevents the script from outrunning the overlay — if hardware can't keep up, FPS drops but latency stays near 0 ms. Call after overlay_end().
kernal.overlay_end()
kernal.flush() -- script waits here until the frame is on screen
kernal.sync() NEW
Convenience: flush() + yield to event loop. Equivalent to flush() then sleep(0). Use this as your frame-end call for zero-latency scripts that don't need a fixed FPS cap.
kernal.overlay_end()
kernal.sync() -- flush + yield, loop runs as fast as renderer can paint
kernal.overlay_set_clickthrough(enabled) NEW
Toggle click-through mode on the overlay. When false, the overlay receives mouse clicks (for interactive menus). When true (default), clicks pass through to the game.
| Param | Type | Description |
|---|---|---|
enabled | boolean | true = click-through, false = interactive |
kernal.overlay_set_clickthrough(false) -- overlay can now receive clicks
-- ... interactive menu logic ...
kernal.overlay_set_clickthrough(true) -- back to click-through
kernal.set_draw_fps(fps) NEW
Set the overlay draw FPS cap independently from script tick rate. Scripts continue running at full speed (e.g. aimbot at 500 Hz) while overlay redraws are capped (e.g. 144 Hz). Pass 0 to disable the cap.
| Param | Type | Description |
|---|---|---|
fps | number | Target draw FPS (0 = uncapped) |
kernal.set_draw_fps(144) -- overlay at 144 Hz, script ticks unlimited
-- Standard overlay loop pattern:
kernal.overlay_open()
while true do
kernal.overlay_begin()
-- ... your draw calls here ...
kernal.draw_text("Hello!", 10, 10, "#FFFFFF", 16)
kernal.overlay_end()
kernal.sync() -- zero-latency frame pacing
end
kernal.overlay_close()
Overlay — Drawing Primitives
All draw functions must be called between overlay_begin() and overlay_end(). Colors are CSS-format hex strings (e.g. "#FF0000" for red). Coordinates are in screen pixels (0,0 = top-left).
kernal.draw_rect(x, y, w, h, color, thickness)
Draw a rectangle outline. The bread-and-butter for bounding boxes.
| Param | Type | Default | Description |
|---|---|---|---|
x, y | number | — | Top-left corner |
w, h | number | — | Width and height |
color | string | "#FF0000" | Outline color |
thickness | number | 2 | Line width in pixels |
-- Enemy bounding box
kernal.draw_rect(screenX - w/2, screenY, w, h, "#FF4444", 2)
kernal.draw_rect_fill(x, y, w, h, color)
Draw a filled rectangle. Great for health bars, backgrounds, and indicators.
-- Health bar with background
kernal.draw_rect_fill(10, 10, 200, 20, "#333333") -- bg
kernal.draw_rect_fill(10, 10, 200 * (hp/100), 20, "#44FF44") -- fill
kernal.draw_rect_rounded(x, y, w, h, radius, color, thickness) NEW
Draw a rounded rectangle outline. Great for modern-looking menu panels.
| Param | Type | Default | Description |
|---|---|---|---|
x, y | number | — | Top-left corner |
w, h | number | — | Width and height |
radius | number | 5 | Corner radius |
color | string | "#FF0000" | Outline color |
thickness | number | 2 | Line width |
kernal.draw_rect_rounded(100, 100, 200, 150, 10, "#FFFFFF", 2)
kernal.draw_line(x1, y1, x2, y2, color, thickness)
Draw a line between two points. Used for snaplines (tracers from screen edge to entities).
| Param | Type | Default | Description |
|---|---|---|---|
x1, y1 | number | — | Start point |
x2, y2 | number | — | End point |
color | string | "#FF0000" | Line color |
thickness | number | 2 | Line width |
-- Snapline from bottom-center of screen to player
local SW = kernal.screen_width()
local SH = kernal.screen_height()
kernal.draw_line(SW/2, SH, playerX, playerY, "#FFFF00", 1)
kernal.draw_circle(x, y, r, color, thickness)
Draw a circle outline.
| Param | Type | Default | Description |
|---|---|---|---|
x, y | number | — | Center point |
r | number | — | Radius in pixels |
color | string | "#FF0000" | Circle color |
thickness | number | 2 | Line width |
-- Crosshair circle
kernal.draw_circle(SW/2, SH/2, 5, "#00FFFF", 1)
kernal.draw_circle_fill(x, y, r, color) NEW
Draw a filled circle. Ideal for head dots, points of interest, or indicators.
-- Head dot on enemy
kernal.draw_circle_fill(headX, headY, 4, "#FF0000")
-- Indicator dot
kernal.draw_circle_fill(20, 20, 6, "#44FF44")
kernal.draw_text(text, x, y, color, size)
Render text on the overlay. Use for labels, status info, and debug data.
| Param | Type | Default | Description |
|---|---|---|---|
text | string | — | Text to display |
x, y | number | — | Position (top-left of text) |
color | string | "#FFFFFF" | Text color |
size | number | 14 | Font size in pixels |
-- HUD title
kernal.draw_text("KERNAL ESP", 10, 20, "#00FF00", 18)
-- Player name above box
kernal.draw_text(playerName, boxX, boxY - 14, "#FFFFFF", 11)
-- Distance label
kernal.draw_text(math.floor(dist) .. "m", boxX, boxY + boxH + 2, "#AAAAAA", 10)
kernal.draw_text_centered(text, x, y, color, size) NEW
Draw center-aligned text. The x coordinate is the center point, not the left edge.
kernal.draw_text_centered("MENU TITLE", SW/2, 50, "#FFFFFF", 24)
kernal.text_width(text, size) NEW
Returns the approximate pixel width of a string at the given font size. Useful for menu layout math.
Returns:number (pixel width)
local w = kernal.text_width("Hello World", 14)
kernal.draw_progress_bar(x, y, w, h, fraction, fgColor, bgColor) NEW
Draw a progress/health bar with background and foreground fill.
| Param | Type | Default | Description |
|---|---|---|---|
x, y | number | — | Top-left corner |
w, h | number | — | Full bar dimensions |
fraction | number | 0 | Fill amount (0.0 to 1.0) |
fgColor | string | "#00FF00" | Foreground (fill) color |
bgColor | string | "#333333" | Background color |
kernal.draw_progress_bar(100, 200, 200, 20, hp / 100, "#44FF44", "#333333")
kernal.draw_image(path, x, y, w, h) NEW
Render a PNG or image file on the overlay.
| Param | Type | Description |
|---|---|---|
path | string | File path or URL to image |
x, y | number | Top-left corner |
w, h | number | Render dimensions |
kernal.draw_image("C:/path/to/logo.png", 10, 10, 64, 64)
Input — Keyboard & Keys
Check keyboard and mouse button states for toggles, hotkeys, and interactive scripts. All key checks are non-blocking and return the current state instantly.
kernal.key_down(key) NEW
Check if a key is currently held down. Supports key names and virtual key codes.
| Param | Type | Description |
|---|---|---|
key | string | number | Key name ("F1", "SHIFT", "MOUSE4") or VK code |
boolean
Supported key names: A–Z, 0–9, F1–F12, SHIFT, CTRL, ALT, SPACE, ENTER, ESC, TAB, MOUSE4, MOUSE5, LBUTTON, RBUTTON, MBUTTON, UP, DOWN, LEFT, RIGHT, INSERT, DELETE, HOME, END, PAGEUP, PAGEDOWN, CAPSLOCK, NUMPAD0–NUMPAD9, LSHIFT, RSHIFT, LCONTROL, RCONTROL, LALT, RALT
if kernal.key_down("SHIFT") then
kernal.log("Shift is held!")
end
kernal.key_pressed(key) NEW
Edge-triggered key check — returns true once on the frame the key was pressed, not while held.
boolean
if kernal.key_pressed("F1") then
espEnabled = not espEnabled
end
kernal.mouse_pos() NEW
Get current cursor position on screen.
Returns:table with x, y
local mp = kernal.mouse_pos()
kernal.log("Cursor at: " .. mp.x .. ", " .. mp.y)
kernal.key_states(...) NEW
Batch key check — queries multiple keys in a single IPC call. Use this when you need to check 3+ keys per frame to avoid sequential round-trips.
| Param | Type | Description |
|---|---|---|
... | string | number | Key names or VK codes (variadic) |
table (0-indexed) of {down, pressed} for each key
local keys = kernal.key_states("LBUTTON", "SHIFT", "F1")
if keys[0].down then -- fire held
-- aimbot logic
end
if keys[2].pressed then -- F1 toggled
espEnabled = not espEnabled
end
kernal.read_inputs(keys) NEW
Combined input read — key states and mouse cursor in one IPC call. Ideal for the top of your game loop to gather all input state at once.
| Param | Type | Description |
|---|---|---|
keys | table | List of key names or VK codes |
Returns: Flat table (avoids nested-object issues):
| Field | Type | Description |
|---|---|---|
k0_d | boolean | Key 0 down (held) |
k0_p | boolean | Key 0 pressed (edge) |
k1_d, k1_p | boolean | Key 1 states |
mx | number | Mouse X |
my | number | Mouse Y |
n | number | Number of keys queried |
local inp = kernal.read_inputs({"LBUTTON", "SHIFT", "F1"})
local fire = inp.k0_d -- LBUTTON held
local shift = inp.k1_d -- SHIFT held
local toggle = inp.k2_p -- F1 edge-triggered
local mx, my = inp.mx, inp.my
Mouse & Aimbot
Functions for programmatic mouse control. Used for recoil compensation, aimbot smoothing, and automated clicking.
kernal.mouse_move(dx, dy) NEW
Relative mouse movement via SendInput. Bypasses game raw input hooks — ideal for RCS and aimbot smoothing.
| Param | Type | Description |
|---|---|---|
dx | number | Horizontal pixels to move |
dy | number | Vertical pixels to move |
-- Simple RCS: compensate recoil
kernal.mouse_move(0, -2)
kernal.mouse_move_to(x, y) NEW
Move cursor to absolute screen position.
| Param | Type | Description |
|---|---|---|
x | number | Screen X coordinate |
y | number | Screen Y coordinate |
kernal.mouse_move_to(SW/2, SH/2) -- center of screen
kernal.mouse_click(button) NEW
Simulate a mouse button click (down + up).
| Param | Type | Default | Description |
|---|---|---|---|
button | string | "left" | "left", "right", or "middle" |
kernal.mouse_click("left")
Script Lifecycle
Control how your script starts, stops, and manages frame timing. These functions help you write clean, well-behaved scripts that release resources properly.
kernal.on_stop(fn) NEW
Register a cleanup callback that runs when the script is stopped (by the user or by an error). Guaranteed to fire — use it for overlay_close, detach, etc.
kernal.on_stop(function()
kernal.overlay_close()
kernal.detach()
kernal.log("Cleaned up!")
end)
kernal.set_fps(n) NEW
Set a target frame rate. Use with delta_time() for frame-rate-independent logic. This sets internal state — you still need sleep() in your loop.
| Param | Type | Description |
|---|---|---|
n | number | Target FPS (0 = unlimited) |
kernal.set_fps(144) -- target 144 FPS
kernal.delta_time() NEW
Returns milliseconds since the last call to delta_time(). Useful for smooth interpolation and frame-rate-independent logic.
number (ms)
local dt = kernal.delta_time()
local smoothFactor = dt / 16.0 -- normalize to ~60 FPS
Display Info
Query the physical screen resolution. These values account for DPI scaling and return the actual pixel dimensions your overlay renders at.
kernal.screen_width()
Returns: number (pixels)
kernal.screen_height()
Returns: number (pixels)
local SW = kernal.screen_width() -- e.g. 2560
local SH = kernal.screen_height() -- e.g. 1440
kernal.log(string.format("Resolution: %dx%d", SW, SH))
-- Center of screen (useful for crosshairs, snaplines)
local cx, cy = SW/2, SH/2
Offsets
Game offsets are memory addresses for specific structures (entity lists, view matrices, player data). They change with every game update and must be kept current for your scripts to work.
Where to find offsets:
- Kernal website — live-updated offsets at kernal.vip/offsets
- In-client viewer — RE Suite panel shows current offsets
- Dump them yourself — pattern scanners, offset dumpers, reverse engineering
- Community repos — GitHub projects that update after each game patch
kernal.get_offset(game, key)
Retrieve a named offset from the injected offset table (populated by the Kernal UI when launching scripts — future feature). Currently returns nil unless offsets are passed by the launcher.
| Param | Type | Description |
|---|---|---|
game | string | Game identifier |
key | string | Offset name |
number or nil
-- Recommended: hardcode offsets at the top of your script
-- Update these when the game patches
local ENTITY_LIST = 0x24D0DC0
local LOCAL_PLAYER = 0x2056700
local VIEW_MATRIX = 0x2330AE0
local HEALTH = 0x34C
local TEAM = 0x3EB
local POSITION = 0x1390
Latency & Flow Control
The overlay pipeline uses ack-based flow control to prevent latency buildup. Understanding this is critical for writing smooth, low-latency scripts.
The Problem
When a script runs faster than the overlay renderer can paint (common on 300+ FPS hardware), draw commands queue up in Chromium's IPC buffer. The overlay renders frames in order — including stale ones from seconds ago — creating a growing delay between game state and what's drawn on screen.
The Solution
The main process only sends a new frame to the renderer after the renderer acknowledges the previous one. At most 2 frames exist at any time: 1 being painted + 1 pending (latest-only). Older pending frames are silently replaced.
Usage Patterns
Pattern 1: Zero-latency ESP (recommended for most scripts)
for tick = 1, 999999 do
-- ... read game data ...
kernal.overlay_begin()
-- ... draw commands ...
kernal.overlay_end()
kernal.sync() -- waits for paint, then yields — latency stays ~0 ms
end
Pattern 2: Decoupled aimbot + overlay
kernal.set_draw_fps(144) -- overlay capped at 144 Hz
for tick = 1, 999999 do
-- ... read game data ...
kernal.overlay_begin()
-- ... draw commands ...
kernal.overlay_end() -- skipped if < 1/144s since last drawn frame
-- ... aimbot logic (runs every tick, not capped) ...
kernal.sleep(0) -- tick as fast as possible for aimbot
end
Pattern 3: Frame-locked rendering
kernal.overlay_end()
kernal.flush() -- block until THIS frame is painted
-- guaranteed: next read_multi gets data from AFTER the frame was displayed
Safety
If the renderer crashes or freezes, a 100 ms safety timeout automatically clears the ack gate so the script doesn't hang permanently.
Performance Tips
Each kernal.* memory call is an IPC round-trip to the helper process (~1–2 ms). At 50 calls per frame, that's 50–100 ms of latency — making your overlay feel sluggish. Here's how to stay fast:
- Use
read_multi()for everything — batch ALL per-frame reads (HP, team, position, view matrix) into ONE call. This is the single biggest optimization. 1 IPC ≈ 1.5 ms. 7 IPC ≈ 10 ms. - Read view matrix LAST — put it at the end of your
read_multiso the helper reads it at the very end. This gives you the freshest camera data. - Cache entity resolution — controller → pawn pointer lookups rarely change (only when players join or leave). Resolve them every 25–100 frames, not every frame.
- Use
sync()afteroverlay_end()for zero-latency rendering — the script waits for the renderer to finish painting before the next tick. If the renderer can't keep up, FPS drops but latency stays ~0 ms. - Use
sleep(0)only if you don't care about overlay latency (e.g. pure aimbot with no ESP). Usesleep(16)to save CPU. - Use
set_draw_fps()to decouple aimbot from overlay — run your aimbot at 500 Hz while the overlay only redraws at 144 Hz. - Use
read_buf()for contiguous data — read a whole struct (health, team, position) in one call instead of 3 separate reads. - Wrap player reads in
pcall()— players disconnect mid-frame and their memory becomes invalid.pcallcatches the error instead of crashing your script. - Use
read_inputs()to get key states + mouse position in a single call — collapses 3–5 calls into 1. - Use
on_stop()for cleanup — guaranteesoverlay_close()anddetach()run even on errors. - Binary pipe — memory reads automatically use a high-speed binary channel when available (v0.12+), eliminating hex encode/decode overhead.
- Don't log every frame —
kernal.login a 500 Hz loop floods the UI. Log only on state changes or every N frames. - Flush vs. Sync —
flush()blocks until the current frame is painted (use for frame-locked scripts).sync()doesflush()+ yield (use as your main loop frame-end). Both prevent the 5+ second latency that occurs when scripts outrun the overlay renderer.
Error Handling
Memory reads throw Lua errors when the address is invalid, the process has exited, or you're not attached. Use pcall to handle these gracefully without crashing your script.
-- Safe read with error handling
local ok, val = pcall(function()
return kernal.read_i32(addr)
end)
if ok then
kernal.log("Value: " .. val)
else
kernal.warn("Read failed: " .. tostring(val))
end
-- Common pattern: wrap each entity in pcall
-- so one bad pointer doesn't kill the whole loop
for i = 0, 63 do
pcall(function()
local entity = getEntity(i)
if entity == 0 then return end
-- ... read and draw ...
end)
end
API Version
Query the current API version supported by the running Kernal instance:
kernal.log("API version: " .. kernal.version) -- "0.3"
Complete Example — CS2 ESP
A fully working external ESP overlay with enemy boxes, health bars, and HP text. Uses entity caching and a mega read_multi for 1 IPC call per frame on normal ticks. Targets CS2 but the pattern applies to any game.
Key techniques:
- Entity resolution is cached and only refreshed every N frames (avoids 4–5 extra IPC calls)
- All per-frame data (player HP, team, position, view matrix) is read in ONE
read_multi - View matrix is read last so it's the freshest value when drawing
pcallguards against stale pointers (players disconnecting mid-frame)sync()for zero-latency rendering (script paces itself to the renderer)
-- CS2 Simple Enemy ESP (optimized — 1 IPC/frame, zero-latency overlay)
-- Offsets (update after game patches)
local dwEntityList = 0x24D0DC0
local dwLocalPlayerPawn = 0x2056700
local dwViewMatrix = 0x2330AE0
local m_iHealth = 0x34C
local m_iTeamNum = 0x3EB
local m_vOldOrigin = 0x1390
local m_hPlayerPawn = 0x90C
local ENT_STRIDE = 0x70
local CACHE_EVERY = 25
-- Setup
kernal.attach("cs2.exe")
local base = kernal.module_base("client.dll")
local SW = kernal.screen_width()
local SH = kernal.screen_height()
local SW2, SH2 = SW/2, SH/2
kernal.overlay_open()
kernal.on_stop(function()
kernal.overlay_close()
kernal.detach()
end)
-- Entity list pointers (stable, refresh periodically)
local entity_list = kernal.read_i64(base + dwEntityList)
local list_entry = kernal.read_i64(entity_list + 0x10)
-- Entity cache: resolved pawn addresses
local cachedPawns = {}
local lastLocalPawn = 0
-- World to screen projection
local function w2s(px, py, pz, vm)
local w = vm[12]*px + vm[13]*py + vm[14]*pz + vm[15]
if w < 0.01 then return nil, nil end
return SW2 + (vm[0]*px + vm[1]*py + vm[2]*pz + vm[3]) / w * SW2,
SH2 - (vm[4]*px + vm[5]*py + vm[6]*pz + vm[7]) / w * SH2
end
-- Main loop
for tick = 1, 999999 do
-- Refresh cache every N frames (entity resolution = extra IPC)
if tick % CACHE_EVERY == 1 or #cachedPawns == 0 then
-- ... resolve controller → pawn pointers ...
-- (batch read handles, resolve chunks, cache valid pawns)
end
-- ONE IPC: all player data + localTeam + view matrix
local megaA = {}
megaA[1] = lastLocalPawn + m_iTeamNum; megaA[2] = 4
for _, pawn in ipairs(cachedPawns) do
megaA[#megaA+1] = pawn + m_iHealth; megaA[#megaA+1] = 4
megaA[#megaA+1] = pawn + m_iTeamNum; megaA[#megaA+1] = 4
megaA[#megaA+1] = pawn + m_vOldOrigin; megaA[#megaA+1] = 12
end
-- View matrix LAST for maximum freshness
megaA[#megaA+1] = base + dwViewMatrix; megaA[#megaA+1] = 64
local ok, buf = pcall(kernal.read_multi, table.unpack(megaA))
if not ok or not buf then
kernal.overlay_begin(); kernal.overlay_end()
kernal.sync(); goto continue
end
-- Parse view matrix from end of buffer
local vmOff = 4 + #cachedPawns * 20
local vm = {}
for i = 0, 15 do vm[i] = buf.f32(vmOff + i*4) end
-- Draw
kernal.overlay_begin()
local localTeam = buf.i32(0)
for n, pawn in ipairs(cachedPawns) do
pcall(function()
local off = 4 + (n-1) * 20
local hp = buf.i32(off)
local team = buf.i32(off + 4)
local px = buf.f32(off + 8)
local py = buf.f32(off + 12)
local pz = buf.f32(off + 16)
if team == localTeam then return end
if hp <= 0 or hp > 100 then return end
local sfx, sfy = w2s(px, py, pz, vm)
local shx, shy = w2s(px, py, pz + 72, vm)
if not (sfx and shx) then return end
local boxH = sfy - shy
if boxH < 5 then return end
local boxW = boxH / 2.4
kernal.draw_rect(shx - boxW/2, shy, boxW, boxH, "#FF4444", 2)
local hpFrac = hp / 100
local barH = boxH * hpFrac
local hpColor = "#44FF44"
if hpFrac <= 0.25 then hpColor = "#FF4444"
elseif hpFrac <= 0.5 then hpColor = "#FFAA00" end
kernal.draw_rect_fill(shx - boxW/2 - 6, sfy - barH, 3, barH, hpColor)
kernal.draw_text(hp .. " HP", shx - boxW/2, shy - 14, "#FFFFFF", 11)
end)
end
kernal.overlay_end()
kernal.sync() -- wait for renderer to paint — zero latency
::continue::
end
Complete Example — Apex Legends ESP
A minimal, high-performance external ESP for Apex Legends. Box + health bar + shield bar only. Uses split scanning (fast player scan every frame, slow bot scan in background) and a single read_multi mega-read for all entity data.
Key techniques:
- Players are always in entity slots 0–127 (read all 128 pointers in one
read_buf) - Bots/dummies live in slots 128+ (scanned incrementally in background, 1 chunk per frame)
- All per-frame entity data packed into one
read_multicall - View matrix read via pointer dereference (Apex uses ViewRender → matrixPtr → float[16])
sync()for zero-latency overlay rendering
-- Apex Legends ESP — box + health + shield (zero-latency overlay)
-- Offsets (update after game patches)
local OFF = {
cl_entitylist = 0x6266928,
local_player = 0x268FA08,
view_render = 0x3D97D78,
view_matrix_off = 0x11A350,
m_iHealth = 0x0324,
m_iMaxHealth = 0x0468,
m_iTeamNum = 0x0334,
m_vecAbsOrigin = 0x017C,
m_lifeState = 0x0690,
m_bleedoutState = 0x27E0,
m_shieldHealth = 0x01A0,
m_shieldHealthMax = 0x01A4,
}
-- Setup
kernal.attach("r5apex_dx12.exe")
local base = kernal.module_base("r5apex_dx12.exe")
local SW = kernal.screen_width()
local SH = kernal.screen_height()
local SW2, SH2 = SW / 2, SH / 2
kernal.overlay_open()
kernal.on_stop(function()
kernal.overlay_close()
kernal.detach()
end)
-- Fast player scan (slots 0-127, every frame)
local function scanPlayers(localPlayer)
local ptrBuf = kernal.read_buf(base + OFF.cl_entitylist, 128 * 32)
local ptrs, count = {}, 0
for i = 0, 127 do
local entPtr = ptrBuf.i64(i * 32)
if entPtr ~= 0 and entPtr ~= localPlayer then
count = count + 1
ptrs[count] = entPtr
end
end
return ptrs, count
end
-- World to screen
local vm = {[0]=0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}
local function w2s(px, py, pz)
local w = vm[12]*px + vm[13]*py + vm[14]*pz + vm[15]
if w < 0.01 then return nil, nil end
local invW = 1.0 / w
return SW2 + (vm[0]*px + vm[1]*py + vm[2]*pz + vm[3]) * invW * SW2,
SH2 - (vm[4]*px + vm[5]*py + vm[6]*pz + vm[7]) * invW * SH2
end
-- Main loop
for tick = 1, 999999 do
local localPlayer = kernal.read_i64(base + OFF.local_player)
if localPlayer == 0 then
kernal.overlay_begin(); kernal.overlay_end()
kernal.sync(); goto continue
end
local entities, entCount = scanPlayers(localPlayer)
-- Mega read: localTeam + per-entity data + view render ptr
-- STRIDE = hp(4)+maxHp(4)+team(4)+life(4)+shield(4)+shieldMax(4)+origin(12)+bleedout(4) = 40
local STRIDE = 40
-- ... build megaA, read_multi, parse buffer, draw boxes + bars ...
-- View matrix via pointer chain (Apex-specific)
local vrPtr = kernal.read_i64(base + OFF.view_render)
if vrPtr ~= 0 then
local matPtr = kernal.read_i64(vrPtr + OFF.view_matrix_off)
if matPtr ~= 0 then
local vmBuf = kernal.read_buf(matPtr, 64)
for i = 0, 15 do vm[i] = vmBuf.f32(i*4) end
end
end
kernal.overlay_begin()
-- Draw entities with boxes, health bars, shield bars ...
kernal.overlay_end()
kernal.sync()
::continue::
end