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.

All 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.

ParamTypeDescription
msnumberMilliseconds 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.

ParamTypeDescription
pidOrNamenumber | stringProcess ID or executable name (e.g. "game.exe")
Returns: 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.

Returns: 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.

ParamTypeDescription
addrnumberMemory address to read from
lennumberNumber of bytes to read
Returns: 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.

ParamTypeDescription
addrnumberStart address
lennumberNumber of bytes to read
Returns: Buffer object

Buffer methods:

MethodDescription
.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.

ParamTypeDescription
addr1numberFirst address to read
len1numberBytes to read from addr1
......More (address, length) pairs
Returns: 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).

ParamTypeDescription
addrnumberTarget address
hexstringHex-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.

ParamTypeDescription
addrnumberTarget address
valuenumberFloat 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.

ParamTypeDescription
addrnumberTarget address
valuenumber64-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.

ParamTypeDescription
namestringModule filename (e.g. "client.dll", "engine2.dll")
Returns: 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.

Returns: 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.

ParamTypeDefaultDescription
x, ynumberTop-left corner
w, hnumberWidth and height
colorstring"#FF0000"Outline color
thicknessnumber2Line 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).

ParamTypeDefaultDescription
x1, y1numberStart point
x2, y2numberEnd point
colorstring"#FF0000"Line color
thicknessnumber2Line 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.

ParamTypeDefaultDescription
x, ynumberCenter point
rnumberRadius in pixels
colorstring"#FF0000"Circle color
thicknessnumber2Line 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.

ParamTypeDefaultDescription
textstringText to display
x, ynumberPosition (top-left of text)
colorstring"#FFFFFF"Text color
sizenumber14Font 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.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.

ParamTypeDescription
gamestringGame identifier
keystringOffset name
Returns: 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:

  1. 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.
  2. Use read_multi() for scattered addresses — batch reads from different locations into a single round-trip. Reading 5 player healths? One read_multi call instead of 5 read_i32 calls.
  3. 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.
  4. Wrap player reads in pcall() — players disconnect mid-frame, making their pointers invalid. pcall catches the error silently instead of crashing your script.
  5. Control your tick ratesleep(1) = max speed, sleep(16) = 60fps. For ESP you rarely need more than 144fps. Higher sleep = less CPU = less detection surface.
  6. Minimize draw calls per frame — only draw entities that are on-screen. Check world-to-screen results for nil before 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"
Kernal © 2026 Kernal · v1.0.20 · kernal.vip