Violet

Render

On this page

core.render draws shapes and text directly over the game window: ESP boxes on mobs, HP bars, HUD text, path overlays, target markers. Drawing is stateless — everything you queue is shown for one frame and gone the next — so you draw every frame from the on_render() callback, which is the only place drawing calls are accepted.

Draw from `on_render()`

Define on_render() in your script. It runs once per presented frame, after that frame's on_tick if one ran, and only while the overlay is able to draw. Drawing calls made anywhere else — on_tick, on_map_load, a key callback — draw nothing and return nil, reason. Keep game logic in on_tick: read what you need there, store it, and draw it in on_render().

lua
function on_render()
    core.render.text(20, 20, "Hello from Violet", { size = 18, shadow = true })
end

How it works

  • Coordinates are screen pixels by default, with (0, 0) at the top-left of the game window. Pass world = true in the options table to give world coordinates instead; the renderer converts them on the frame it actually draws.
  • Shapes live for one frame. Every draw call queues a shape for the frame being built; on_render() runs again for the next one. If you stop calling, the shape disappears. There is nothing to delete.
  • Drawing is only accepted inside on_render(). Anywhere else the drawing functions return nil, reason and queue nothing, so a script cannot half-work by drawing from on_tick.
  • on_render() runs at frame rate, not tick rate. Keep it cheap: no searches, no network, no settings writes. A callback that raises an error is reported to the console once and removed until the script is reloaded, so a bug cannot spam every frame.
  • Colours are {r, g, b} or {r, g, b, a} with 0–255 components. Alpha defaults to 255 (opaque). The default colour is white.
  • Sizes follow the coordinate space. For a world-space shape, width, height, and radius are world units and zoom with the game. Line thickness and font size are always pixels, so outlines never thin out when the camera zooms.
  • Unloading a script removes its drawings. Each shape remembers which script queued it.
World-space needs a view

World-space shapes are skipped on frames where the engine hasn't published a view yet (the first frames after injection, or outside a field). Screen-space shapes always draw once core.render.is_ready() is true.

Options table

Every drawing function takes an optional trailing opts table. Unknown fields are ignored; missing fields use the defaults below.

FieldTypeDefaultApplies toMeaning
colortable{255, 255, 255}all{r, g, b[, a]}, 0–255
color2tablecolorgradientSecond colour of the gradient
thicknessnumber1outlines, lineStroke width in pixels (minimum 0.1)
roundingnumber0rect, rect_filled, gradientCorner radius
glownumber0allSoft halo drawn around the shape, in pixels (clamped to 32)
segmentsnumber0circle, circle_filledCircle segment count; 0 lets the renderer choose
sizenumber0textFont size in pixels; 0 uses the overlay's default font size
shadowbooleanfalsetextDark drop shadow, keeps text legible over bright game art
centeredbooleanfalsetextCentre the text on (x, y) instead of starting there
worldbooleanfalseallTreat the coordinates as world units
anchornumber0allHandle from mob:anchor() / player:anchor(). Implies world = true

Drawing

All drawing functions return true, or nil, reason when called outside on_render().

core.render.line(x0: number, y0: number, x1: number, y1: number, opts?: table)

Draws a line between two points.


core.render.rect(x: number, y: number, w: number, h: number, opts?: table)

Draws a rectangle outline with its top-left corner at (x, y).


core.render.rect_filled(x: number, y: number, w: number, h: number, opts?: table)

Draws a filled rectangle with its top-left corner at (x, y).


core.render.gradient(x: number, y: number, w: number, h: number, opts?: table)

Draws a filled rectangle that fades from opts.color to opts.color2.


core.render.circle(x: number, y: number, radius: number, opts?: table)

Draws a circle outline centred on (x, y). radius must not be negative.


core.render.circle_filled(x: number, y: number, radius: number, opts?: table)

Draws a filled circle centred on (x, y). radius must not be negative.


core.render.triangle(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, opts?: table)

Draws a triangle outline through three points.


core.render.triangle_filled(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, opts?: table)

Draws a filled triangle through three points.


core.render.text(x: number, y: number, text: string, opts?: table)

Draws a string with its top-left corner at (x, y), or centred on it with centered = true. Text longer than 512 characters is truncated.

lua
function on_render()
    local w = core.render.screen_size()
    core.render.text(w / 2, 40, "BOSS SPAWNED", {
        size = 24, centered = true, shadow = true, color = { 255, 80, 80 },
    })
end

core.render.clear()

Discards every shape the calling script has queued so far this frame. Useful when an early draw pass must be replaced by a later decision in the same on_render() call.

Screen and camera

These read the view the render thread sampled for the current frame, so they are cheap to call many times per tick and never touch the engine from the script thread.

core.render.screen_size() -> number, number

Returns the overlay's width and height in pixels (the size of the game's back buffer). Both are 0 before the overlay is ready.


core.render.is_ready() -> boolean

Returns true once the overlay has captured the game's device and can draw. on_render() is only called while this is true, so scripts rarely need to check it themselves.


core.render.measure_text(text: string, size?: number) -> number, number

Returns the width and height in pixels that text would occupy at size (or the default font size when omitted or 0). Returns nil, error while the overlay is not ready.

lua
local label = "HP 100%"
local tw, th = core.render.measure_text(label, 14)
if tw then
    core.render.rect_filled(x - 4, y - 4, tw + 8, th + 8, { color = { 0, 0, 0, 160 } })
    core.render.text(x, y, label, { size = 14 })
end

core.render.camera() -> number, number

Returns the world coordinate the view is centred on for the current frame. Returns nil, error when no view is available.


core.render.viewport() -> number, number, number

Returns the width and height, in world units, that the game composes the view at, plus the engine's vertical adjust_y term. These differ from screen_size() because the engine zooms the composed view to fill the window. Returns nil, error when no view is available.


core.render.view_scale() -> number, number, number

Returns scale, origin_x, origin_y. scale is screen pixels per world unit for the current frame, and (origin_x, origin_y) is the world coordinate drawn at the top-left of the window. A world position maps to the screen as (wx - origin_x) * scale, (wy - origin_y) * scale. Returns nil, error when no view is available.


core.render.world_to_screen(world_x: number, world_y: number) -> number, number, boolean

Converts a world position to screen pixels for the current frame. Returns x, y, on_screen; the coordinates stay valid off-screen, so you can draw an arrow toward something behind the player. Returns nil, error when no view is available.

Prefer `world = true` or an anchor

This conversion uses the view from the last presented frame. When you draw the shape yourself with world = true, or pin it with an anchor, the renderer converts against the frame it is actually drawing, which is one frame fresher. Use world_to_screen() when you need the pixel position for your own logic, such as clamping a marker to the screen edge.

lua
function on_render()
    local map = core.object_manager.get_current_map()
    if not map then return end
    local sx, sy, visible = core.render.world_to_screen(map:get_bounds().center.x, 0)
    if sx and not visible then
        local w = core.render.screen_size()
        core.render.text(math.clamp(sx, 20, w - 20), 60, "map centre ->", { centered = true })
    end
end

Anchors: drawing on moving actors

get_position() on a mob or the player advances once per logic step (~30 ms) — even when read from on_render(), it is the last step's value — but the sprite is drawn from a per-frame interpolated position. A shape placed at get_position() therefore trails a moving actor by up to one step. An anchor fixes this: it pins the shape to the actor, and the renderer re-reads where the actor is drawn on every frame and shifts the shape to match.

  • Get a handle with mob:anchor() or player:anchor().
  • Pass it as the anchor option. Anchored shapes are automatically world-space, so give world coordinates.
  • Draw the shape at the actor's get_position(); the anchor supplies the correction.
Handles last one frame

A handle is only valid during the on_render() call it was created in. Call anchor() again every frame; caching a handle across frames will silently pin your shape to a different actor. Pass the number, not the mob or player object — a non-number is ignored and the shape draws unanchored.

lua
function on_render()
    local map = core.object_manager.get_current_map()
    if not map then return end

    for _, mob in ipairs(map:get_mobs()) do
        if mob and mob:is_valid() then
            local pos = mob:get_position()
            local a = mob:anchor()
            if pos and a then
                -- 60x80 box around the mob, following the sprite
                core.render.rect(pos.x - 30, pos.y - 80, 60, 80, {
                    anchor = a, color = { 255, 60, 60 }, thickness = 2,
                })

                local hp = mob:get_health_percent()
                if hp then
                    core.render.rect_filled(pos.x - 30, pos.y + 4, 60 * hp / 100, 4, {
                        anchor = a, color = { 80, 255, 120 },
                    })
                end

                local name = mob:get_name()
                if name then
                    core.render.text(pos.x, pos.y - 92, name, {
                        anchor = a, centered = true, shadow = true, size = 12,
                    })
                end
            end
        end
    end
end

To read the interpolated position yourself — for example to draw a line from the player to a mob — use mob:get_render_position() / player:get_render_position(). Anchoring the line's endpoints is not possible (a shape has one anchor), so this is the way to get both ends right.

Full example

lua
plugin = {
    name = "ESP Demo",
    version = "1.0.0",
    author = "you",
    description = "Boxes on mobs, a HUD line, and a player marker",
    load = true,
}

function on_render()
    local w, h = core.render.screen_size()
    core.render.text(16, h - 32, ("Mobs: %d"):format(#(core.object_manager.get_current_map() and core.object_manager.get_current_map():get_mobs() or {})), { shadow = true })

    local player = core.object_manager.get_local_player()
    if player and player:is_valid() then
        local p = player:get_position()
        local a = player:anchor()
        if p and a then
            core.render.circle(p.x, p.y - 30, 40, { anchor = a, color = { 140, 90, 255 }, glow = 6 })
        end
    end
end