Kernal Lua API
Complete reference for the kernal global table available to all scripts. Build overlays, read/write process memory, 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 async 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() 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 high-resolution timestamp in milliseconds. 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
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 table 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 i = 0, 500 do
local p = procs[i]
if p and p.name == "game.exe" then
kernal.log("Found at PID " .. p.pid)
break
end
end
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-2ms 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 |
.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)
Modules
Get information about loaded DLLs/modules in the target process. Module base addresses are the foundation for offset-based memory access — all game structures are 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.
kernal.overlay_close()
Destroy the overlay window. Called automatically when your script stops, but you can call it manually.
-- 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.sleep(1)
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_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)
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
Full Example — Game ESP
A complete, annotated external ESP (Extra Sensory Perception) overlay with bounding boxes, health bars, snaplines, and head dots. This targets CS2 but the pattern applies to any game with an entity list. Every line is commented to explain what's happening.
-- ═══════════════════════════════════════════════════════════
-- External ESP — Full Working Example
-- Game: CS2 (adapt offsets for other titles)
-- ═══════════════════════════════════════════════════════════
-- ▸ OFFSETS — update these after each game patch
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
-- ▸ SETUP
kernal.log("Attaching to cs2.exe...")
kernal.attach("cs2.exe")
local base = kernal.module_base("client.dll")
kernal.log(string.format("client.dll @ 0x%X", base))
local SW = kernal.screen_width()
local SH = kernal.screen_height()
kernal.log(string.format("Screen: %dx%d", SW, SH))
kernal.overlay_open()
-- Read entity list base (stable — only changes on map load)
local entity_list = kernal.read_i64(base + dwEntityList)
local list_entry = kernal.read_i64(entity_list + 0x10)
-- ▸ WORLD-TO-SCREEN
-- Converts 3D world coordinates to 2D screen position
-- using the game's view/projection matrix
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
local sx = (SW/2) + (vm[0]*px + vm[1]*py + vm[2]*pz + vm[3]) / w * (SW/2)
local sy = (SH/2) - (vm[4]*px + vm[5]*py + vm[6]*pz + vm[7]) / w * (SH/2)
return sx, sy
end
-- ▸ MAIN LOOP
kernal.log("ESP running — press Stop to quit")
local playerCount = 0
for tick = 1, 999999 do
kernal.overlay_begin()
kernal.draw_text("KERNAL ESP | Players: " .. playerCount, 10, 20, "#00FF00", 16)
-- Read view matrix (16 floats = 64 bytes)
local vmBuf = kernal.read_buf(base + dwViewMatrix, 64)
local vm = {}
for i = 0, 15 do vm[i] = vmBuf.f32(i * 4) end
-- Read local player to determine our team
local localPawn = kernal.read_i64(base + dwLocalPlayerPawn)
local localTeam = 0
if localPawn ~= 0 then
localTeam = kernal.read_i32(localPawn + m_iTeamNum)
end
-- Bulk read all 64 controller slots in one call
local ctrlBuf = kernal.read_buf(list_entry + ENT_STRIDE, 64 * ENT_STRIDE)
local found = 0
for i = 0, 63 do
pcall(function()
local ctrl = ctrlBuf.i64(i * ENT_STRIDE)
if ctrl == 0 then return end
-- Resolve pawn from controller handle
local pawnHandle = kernal.read_u32(ctrl + m_hPlayerPawn)
if pawnHandle == 0 or pawnHandle == 0xFFFFFFFF then return end
local pawnIdx = pawnHandle & 0x7FFF
local chunkIdx = pawnIdx >> 9
local pawnListEntry = kernal.read_i64(entity_list + 0x10 + 0x8 * chunkIdx)
if pawnListEntry == 0 then return end
local pawn = kernal.read_i64(pawnListEntry + ENT_STRIDE * (pawnIdx & 0x1FF))
if pawn == 0 or pawn == localPawn then return end
-- Read player data
local hp = kernal.read_i32(pawn + m_iHealth)
if hp <= 0 or hp > 100 then return end
local team = kernal.read_i32(pawn + m_iTeamNum)
local isEnemy = (team ~= localTeam)
-- Read 3D position
local posBuf = kernal.read_buf(pawn + m_vOldOrigin, 12)
local px, py, pz = posBuf.f32(0), posBuf.f32(4), posBuf.f32(8)
-- Project feet + head to screen
local sfx, sfy = w2s(px, py, pz, vm)
local shx, shy = w2s(px, py, pz + 72, vm)
if sfx and shx then
found = found + 1
local boxH = sfy - shy
local boxW = boxH / 2.4
local color = isEnemy and "#FF4444" or "#44FF44"
-- Bounding box
kernal.draw_rect(shx - boxW/2, shy, boxW, boxH, color, 2)
-- Health bar (left side)
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)
-- HP text + snapline
kernal.draw_text(hp .. "HP", shx - boxW/2, shy - 4, color, 11)
kernal.draw_line(SW/2, SH, sfx, sfy, color, 1)
end
end)
end
playerCount = found
kernal.overlay_end()
-- Status log every 5 seconds (at ~1000fps = 5000 ticks)
if tick % 300 == 0 then
kernal.log(string.format("tick %d | drawn: %d", tick, found))
end
kernal.sleep(1)
end
kernal.overlay_close()
kernal.detach()
Performance Tips
Each kernal.* memory call is an IPC round-trip to the helper process (~1-2ms). At 50 calls per frame, that's 50-100ms of latency — making your overlay feel sluggish. Here's how to stay fast:
- Use
read_buf()for contiguous data — read a whole struct (health, team, position) in one call instead of 3 separate reads. One 12-byte read is 10x faster than three 4-byte reads. - Use
read_multi()for scattered addresses — batch reads from different locations into a single round-trip. Reading 5 player healths? Oneread_multicall instead of 5read_i32calls. - Cache stable data — module bases, entity list pointers, and controller-to-pawn mappings don't change every frame. Read them once at startup or refresh every few seconds.
- Wrap player reads in
pcall()— players disconnect mid-frame, making their pointers invalid.pcallcatches the error silently instead of crashing your script. - Control your tick rate —
sleep(1)= max speed,sleep(16)= 60fps. For ESP you rarely need more than 144fps. Higher sleep = less CPU = less detection surface. - Minimize draw calls per frame — only draw entities that are on-screen. Check world-to-screen results for
nilbefore drawing.
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.1"