Skip to content

Lua API Reference

The loader's API comes down to two rules.

1. GTA IV natives are global functions, in PascalCase.

SetCharHealth(GetPlayerChar(GetPlayerId()), 200)

The ALL_CAPS form no longer exists as a global function. The canonical SNAKE_CASE name is still reachable through the natives table and through Native.Call.

2. Everything else lives in a namespace.

Thread, Console, Log, Draw, Input, Gamepad, Native, Memory, Vehicle and Script. There are no bare global functions belonging to the loader any more.

Migrating from the old API

print, log, msgbox, is_key_down, draw_text, call_native, CreateThread, Wait… are all nil now. That is deliberate: an old script fails loudly (attempt to call a nil value) instead of failing silently. The full mapping table is at the bottom of this page.


Thread (waiting and coroutines)

The loader ships a coroutine scheduler. It provides the blocking Wait() that ScriptHookDotNet mods are written against, without freezing the game: a waiting thread yields back to the loader, which resumes it once the delay has elapsed.

This is cooperative, and it runs on the game's script thread. It is not an OS thread.

Thread.Create(fn, ...)

Starts a loader-managed coroutine. Arguments after the function are passed to it. Returns an integer id.

  • Example:
    Thread.Create(function()
        while true do
            SetCharHealth(GetPlayerChar(GetPlayerId()), 200)
            Thread.Wait(1000) -- one second, without freezing the game
        end
    end)
    

Thread.Wait(ms)

Suspends the current thread for ms milliseconds. Thread.Wait(0) (or no argument) resumes on the next frame. It works at any call depth inside the thread, and raises an error if called outside a Thread.Create.

Thread.Stop(id)

Stops the thread with that id. Returns true if it was still running. Usable from inside a thread, including on itself.

Thread.Count()

Number of threads still alive in this mod.

Worth knowing

  • A thread created during a tick starts on the next tick.
  • An error raised in a thread kills only that thread: it is logged, and the other threads carry on.
  • The delay is real time: a paused game wakes nothing, but every elapsed delay fires at once on resume. Resolution is one frame, so at 60 fps Thread.Wait(2000) returns between 2000 and 2017 ms.
  • There is a GTA IV native called WAIT, hence a global Wait function. That is one reason for the namespace: Thread.Wait can no longer collide with it. The native stays reachable via Native.Call("WAIT", …).

Console and Log (text output)

Two namespaces, same verb: the destination is in the namespace name.

Console.Print(...)

Writes to the console of the in-game F6 editor. Not to the file. Takes several arguments, joined by a tab. Works from inside a Thread.Create too.

Console.MsgBox(msg)

Modal Windows dialog on top of the game. Blocks the game until dismissed - reserve it for startup or diagnostics.

Log.Print(msg), with Log.Write(msg) as an alias

Writes to the file LuaModLoader.log, next to the game binary. Not to the console.

Errors reach the console on their own

You do not need to catch and reprint errors to see them. Anything that fails at runtime is written to the log and pushed to the editor console: a syntax error at load, an error raised inside a Thread.Create, a native called under a name the game never registered. The console line carries an [erreur] prefix.

Console.Print("shows up in the console")
Log.Print("written to LuaModLoader.log")

Draw (2D rendering)

Drawing is queued into the current frame and rendered once per frame. It is not persistent: call it again on every tick.

Draw.Text(x, y, text, [color], [size], [font])

  • x, y (int): position in pixels, origin top-left.
  • text (string): the text to draw.
  • color (int, optional): 0xAARRGGBB. Default 0xFFFFFFFF (opaque white).
  • size (int, optional): glyph height in pixels. Omitted or 0: the system font, exactly as before.
  • font (string, optional): a GDI family name ("Segoe UI", "Impact", or a family loaded with Draw.LoadFont). Default "Segoe UI" when a size is given.

Draw.TextSize(text, [size], [font]) -> w, h

Measures the text exactly as Draw.Text would draw it, so a menu can right-align a value or centre a title. Returns nil if the measure fails.

Draw.LoadFont(path) -> ok

Loads a .ttf/.otf for this process only (nothing is installed on the machine). The path is relative to the mod folder like images. The family is then addressed by its internal name ("Pricedown"), not by the file name.

Draw.ScreenSize() -> w, h

Size of the game's backbuffer, 0, 0 until the first frame has been rendered. Menus that want GTA V-like proportions scale a 1280x720 layout by h / 720.

Draw.Rect(x, y, w, h, [color])

  • x, y (int): top-left corner, in pixels.
  • w, h (int): width and height.
  • color (int, optional): 0xAARRGGBB. Default 0xFF000000 (opaque black).

Images

PNG, JPEG, BMP and GIF, decoded once and kept in cache -- drawing the same image every frame costs nothing after the first one. Paths are relative to the mod folder (the same root require uses); an absolute path is taken as-is.

Draw.Image(x, y, path, [tint]) Draw.Image(x, y, w, h, path, [tint])

  • x, y (int): top-left corner, in pixels.
  • w, h (int): target size. Pass 0 for both to draw at the file's native size, or 0 for one of them to keep the aspect ratio.
  • path (string): e.g. "ui/button.png".
  • tint (int, optional): 0xAARRGGBB multiplied into the image. Default 0xFFFFFFFF (image unchanged). Because it multiplies, a tint can only darken or fade -- never brighten.

Draw.ImageSize(path) -> w, h, or nil if the file cannot be read. Loads the image as a side effect, which is how you centre or lay out a button.

Draw.LoadImage(path) -> ok, w, h. Decode now rather than on the frame the image first appears. Worth calling from onLoad for a full-screen background: decoding one is a few milliseconds, i.e. a visible hitch exactly when the player opens the menu.

Draw.UnloadImage(path) drops it from the cache. The next draw reads the file again -- which is also how you see an edited image without restarting the game.

Where to put your images

Inside your mod folder, next to your .lua files. mods/mymenu/ui/bg.png is Draw.Image(0, 0, "ui/bg.png"). Forward and backward slashes both work.


Clickable widgets

The pieces needed to build buttons, headers and menu rows. All of them hit-test against the overlay cursor (see Mouse), so they need Input.SetMouseCapture(true) to be useful.

Draw.ImageButton(x, y, w, h, path, [hover_tint], [normal_tint]) -> clicked, hovered

Draws the image, tints it while the cursor is over it, and returns true for clicked on the frame a left click lands inside it. Pass 0 for w or h to take the size from the file.

hover_tint defaults to 0xFFC0C0C0 (slightly dimmed). For a button that lights up instead, ship a dark image and pass 0xFFFFFFFF as hover_tint with 0xFFA0A0A0 as normal_tint.

Draw.IsHovered(x, y, w, h) -> true if the cursor is inside that rectangle. Use it to highlight a row drawn with Draw.Rect.

Draw.Clicked(x, y, w, h, [button]) -> true if a click landed inside that rectangle. Turns any rectangle you have already drawn into a button, no image required. button is 1/"left" (default), 2/"right" or 3/"middle".

A click is consumed by the first widget whose rectangle contains it, and left alone by the others. That is what lets widgets be written one after another with no registry and no z-order bookkeeping -- draw them back to front and the topmost one wins.

local open = false

Thread.Create(function()
    Draw.LoadImage("ui/panel.png")
    while true do
        if Input.IsKeyDown(0x73) then -- F4
            open = not open
            Input.SetMenuOpen(open)
            Input.SetMouseCapture(open) -- rend la souris au jeu en refermant
            Thread.Wait(250)
        end

        if open then
            Draw.Image(100, 100, 400, 300, "ui/panel.png")
            Draw.Text(120, 115, "Menu", Color.RGB(255, 255, 255))

            if Draw.ImageButton(120, 160, 160, 40, "ui/button.png") then
                Console.Print("button clicked")
            end

            -- Une ligne de liste sans image : un rectangle + un test de clic.
            local bg = Draw.IsHovered(120, 220, 360, 28)
                       and Color.RGBA(255, 255, 255, 20)
                       or  Color.RGBA(0, 0, 0, 40)
            Draw.Rect(120, 220, 360, 28, bg)
            Draw.Text(128, 224, "Spawn vehicle")
            if Draw.Clicked(120, 220, 360, 28) then
                Console.Print("row clicked")
            end

            -- Curseur : le jeu n'en dessine pas dans l'overlay, a vous de le faire.
            local mx, my = Input.MousePos()
            Draw.Image(mx, my, 24, 24, "ui/cursor.png")
        end
        Thread.Wait(0)
    end
end)

Color (building colors)

Draw.* expects a single 0xAARRGGBB integer. These two build it from 0-255 components, so a script never has to write a bit shift.

Color.RGB(r, g, b)

An opaque color. Each component is a 0-255 integer (out-of-range values are clamped).

Color.RGBA(r, g, b, [opacity])

The same, except opacity is a percentage from 0 (invisible) to 100 (opaque), not a 0-255 value. Defaults to 100.

Thread.Create(function()
    local bg   = Color.RGBA(0, 0, 0, 70)   -- black at 70% opacity
    local text = Color.RGB(255, 255, 255)  -- opaque white
    while true do
        Draw.Rect(10, 10, 200, 40, bg)
        Draw.Text(20, 20, "LuaModLoader is active", text)
        Thread.Wait(0)
    end
end)

Draw.Text takes x and y first

The order is Draw.Text(x, y, text, color), not (text, x, y), and the color is one argument: Draw.Rect(x, y, w, h, r, g, b, a) raises bad argument.

Screen fades

Everything you draw fades out under the game's black screen, the same way the in-game phone does: leaving an apartment, a mission transition, a load. You get this for free -- there is nothing to call and nothing to check.

The loader reads GET_SCREEN_FADE_ALPHA once per tick and scales the alpha of every queued command by it; at full black nothing is drawn at all. Note that it is the alpha that drops, not the colour: your overlay disappears behind the veil instead of turning grey, which is what makes it look native.


Input / Gamepad

Input.IsKeyDown(vk_code)

true if the Windows virtual-key vk_code is held (e.g. 0x73 for F4). It returns false when the game window is not in the foreground. That is intentional, and it is the most common reason a script "does nothing" during testing. GTA IV must have focus.

Input.SetMenuOpen(open)

Tells the loader your menu is open. While it is, the keys your mod reserved are suppressed for the game (not for Input.IsKeyDown), so the character does not move or shoot while you navigate the menu.

Each mod has its own open/closed state and its own key list, so two menus can reserve different keys without stepping on each other. Call SetMenuOpen(false) when you close the menu; the loader also releases a mod's keys by itself when the mod is stopped or reloaded.

Input.SetMenuKeys{ VK_UP, VK_DOWN, ... }

Keys hidden from the game only while your menu is open. Pass a table or a plain argument list; the values are Windows virtual-key codes, the same ones Input.IsKeyDown takes. Defaults to Up/Down/Left/Right/Enter/Backspace.

Input.SetToggleKeys{ VK_F4 }

Keys hidden from the game at all times, menu open or closed: your open key. The game must never see it, otherwise opening the menu also fires whatever the game binds to that key. Defaults to F4.

Mouse

The loader tracks the cursor in the same pixel coordinates as Draw.*, and queues click edges so none is lost between two ticks.

Input.SetMouseCapture(enabled)

Takes the mouse away from the game: no more camera turning, no more shooting, and the system cursor stays visible instead of being hidden every frame. Call it with false when you close your menu, or the player can no longer aim. The loader releases it by itself when your mod is stopped or reloaded.

Like reserved keys, capture is per-mod: the mouse is captured for the game as long as at least one mod holds it.

Input.MousePos() -> x, y in backbuffer pixels, clamped to the window.

Input.SetMousePos(x, y) moves the loader's cursor (not the system one). Lets a gamepad or the arrow keys drive the same widgets.

Input.IsMouseDown([button]) -> true while the button is held.

Input.MouseClicked([button]) -> clicked, x, y. Consumes one press edge and reports where it happened. Prefer Draw.Clicked / Draw.ImageButton for widgets; this one is for a click anywhere on screen.

Input.MouseWheel() -> accumulated notches since the last call, 120 per notch, negative downwards. Reading it resets it.

In all of these, button is 1/"left" (default), 2/"right" or 3/"middle".

No cursor is drawn for you

The game hides the system cursor while it has focus, and the loader only stops that hiding while the mouse is captured. If your menu is fullscreen or the player is in a cutscene, draw your own cursor with Draw.Image at Input.MousePos() -- it is one line and it always looks right.

Gamepad.IsButtonDown(button)

true if the button is held on pad 0. button is a raw XInput bitmask (e.g. 0x1000 = XINPUT_GAMEPAD_A). Same foreground guard as Input.IsKeyDown.

Gamepad.SetMenuButtons(mask) / Gamepad.SetToggleButtons(mask)

Same two ideas for the pad, as a raw XInput bitmask rather than a list -- the same value Gamepad.IsButtonDown takes, OR-ed together. SetMenuButtons defaults to D-pad + A + B; SetToggleButtons defaults to nothing.

Declaring keys is optional

A mod that never calls these four functions keeps the historical set: F4 at all times, arrows/Enter/Backspace while its menu is open. Nothing to change in an existing mod.

-- Menu opened with F5, navigated with ZQSD, validated with Space.
Input.SetToggleKeys{ 0x74 }                          -- F5
Input.SetMenuKeys{ 0x5A, 0x53, 0x51, 0x44, 0x20 }    -- Z S Q D Space
Gamepad.SetMenuButtons(0x0001 | 0x0002 | 0x1000)     -- D-pad up/down + A

Native (calling natives by name)

Names passed to Native.* are SNAKE_CASE

FindNativeHash() is an exact-match table on the canonical name. Native.Call("TaskPlayAnim", …) would fail silently. It has to be Native.Call("TASK_PLAY_ANIM", …). This is the one ALL_CAPS that survives in the API, and it is not an oversight.

Two reasons to use Native.Call over the typed global:

  • a name built dynamically.
  • a native whose registered signature is wrong. Around sixty natives are described as ScriptAny by public sources, so they are registered as int: the typed global would silently cast a string or a float to an integer. Native.Call pushes arguments as-is, without consulting the signature.

Native.Call(name, ...)

Calls a native by its SNAKE_CASE name. Returns the raw 4 bytes of the return buffer, to be decoded with string.unpack ("<i4", "<f"…). nil if the name is unknown or the call crashed (SEH-guarded engine side).

Native.CallOut(name, outputCount, ...)

Like Native.Call, but allocates outputCount output buffers (max 4) and returns them after the return value. The buffers are contiguous: asking for 3 outputs amounts to handing the native a Vector3.

local _, bx, by, bz = Native.CallOut("GET_PED_BONE_POSITION", 3, ped, bone, 0.0, 0.0, 0.0)
local x = string.unpack("<f", bx)

Native.Probe(name, ...)

Like Native.Call, but never discards the buffer contents: even if the call crashes, whatever was already written is returned. Returns crashed (boolean) then the raw 4 bytes.

Native.Info(name)

Registered signature of a native, or nil. The returned table holds params (each with type, output, pointerInput), hasReturn, returnType, and confidence, which says where the signature came from. Anything that is not x32dbg_live_verified_* should be treated with caution.

Native.Available(name)

true if the native is known and currently resolved in memory. False at the main menu for most gameplay natives.

Native.Address(name [, raw])

Absolute address of the native's handler, 0 if not registered. Diagnostic: the Ghidra address is value - module_base + 0x400000.

The loader hooks one native to get its per-frame tick, by replacing that native's handler in the game's own table. For that one native the table no longer holds a game address, so the default answer is the address the game originally had, which is the one worth decompiling. Pass raw as true to read what the table actually contains right now, which for a hooked native is an address inside LuaModLoader.dll.

Native.StringAddr(s)

Raw address of a Lua string. Used to probe natives whose first argument is itself an input pointer.

Native.Register(name, fn)

Adds a native to the game's own table. Returns its hash, or nil plus a message. Anything that resolves natives by hash can then reach it, whatever language the calling mod is written in.

Native.Register("ADD", function(a, b) return a + b end)

The function receives arguments as raw 32-bit integers, since the game carries no type information. The convention between caller and callee is theirs to agree on.

Three limits, imposed by the game rather than by the loader. The table never grows and cannot free an entry: about 72 slots exist in total, shared by every mod. Re-registering the same name reuses its slot, so hot-reloading costs nothing. And a custom native only runs when called from the Lua tick thread; a call from any other thread is ignored, because entering one Lua state from two threads is undefined behaviour.

Native.CustomHash(name)

The hash that name would get, without registering anything. It is Jenkins one-at-a-time over "LML_" .. name:upper(), five lines to reimplement in any language: that is how a mod written elsewhere calls ours without sharing a single header.

Native.CallHash(hash, ...)

Calls a native by hash instead of by name. Required to reach a custom native, whose hash exists in no static table.

local h = Native.CustomHash("ADD")
local raw = Native.CallHash(h, 2, 3)

natives

Table of every native, keyed by canonical SNAKE_CASE name. This is the only place that form remains callable, as in natives.SET_CHAR_HEALTH(ped, 200).


Memory (reading and writing memory)

Reverse-engineering tooling. Nothing here is needed by an ordinary mod.

Function Purpose
Memory.ModuleInfo([name]) {base, size} of a loaded module; GTAIV.exe with no argument
Memory.FindString(text, …) searches readable memory for an exact string
Memory.FindDword(value, …) same, for a little-endian DWORD
Memory.ReadHex(addr, [count]) hex dump, guarded read
Memory.WriteHex(addr, hex) writes bytes given as hex, guarded write
Memory.ReadU32(addr) reads a little-endian uint32, nil if unreadable
Memory.CallThiscall1(rva, this, arg) calls a __thiscall function in the game
Memory.CallVtableMethod0(obj, offset) calls a virtual method with no arguments

Scans are time-capped, for a reason

Memory.FindString / Memory.FindDword stop after budgetMs (100 ms by default) and return, as a second value, the address to resume from. Without that cap, a default scan over the whole 32-bit user range freezes the game for tens of seconds, because the Lua tick is synchronous. Never loop on resumeAddr inside the same call. Resume from a later tick instead.


Vehicle (body parts)

All of these take a vehicle handle (the same integer as GetCarCharIsUsing), not a pointer.

Function Purpose
Vehicle.BonePrepare(veh) (re)builds the bone cache; returns ok, rebuilt
Vehicle.HasBone(veh, name) does the skeleton contain this bone?
Vehicle.SetBoneVisible(veh, name, visible) shows/hides the part carried by a bone
Vehicle.IsBoneVisible(veh, name) current state; nil = unknown bone
Vehicle.BoneNames(veh, [filter]) table { [name] = index }
Vehicle.HideComponent(veh, index) hides a fragment component (one-way)
Vehicle.RestoreComponents(veh) shows everything that was hidden again
Vehicle.ResolvePtr(veh) resolves the handle to a CVehicle*

Note

The engine recomputes the skeleton pose on a FixCar, a repair, or a re-stream: everything becomes visible again at once. Keeping the name of a hidden bone and re-reading Vehicle.IsBoneVisible occasionally is how you detect it and reapply.


Script (the mod and its execution)

Script.DeclareModFile(relPath)

Adds a file to this mod's manifest.lua script list, so it gets loaded on the next (re)start. Returns false for a buffer launched from the editor, which has no folder.

Script.IsTickFallback()

Always returns false. It used to report a tick running off the game's script thread, on the rendering thread, where natives that need that thread (model streaming, CHANGE_PLAYER_MODEL) could freeze the game. Mods now always tick on the game's own script loop, so that situation no longer exists. The function is kept because mods already call it.

Script.TimeMs()

Milliseconds since process start, as a float (QueryPerformanceCounter). This is Thread.Wait's clock.


Events and globals

A thread is the only per-frame entry point. There is no global the loader calls on every frame, so code that has to run each frame lives in a loop:

Thread.Create(function()
    while true do
        if Input.IsKeyDown(0x73) then -- F4
            -- runs every frame while F4 is held
        end
        Thread.Wait(0)
    end
end)

One entry point means one error policy: an error kills the thread it happened in, and every other thread keeps running.

onUnload()

Called just before the mod's Lua state is closed, on a Stop and on the first half of a Reload. Use it to undo what the mod changed in the world: delete spawned vehicles and peds, remove blips, save settings.

function onUnload()
    if myCar and DoesVehicleExist(myCar) then DeleteCar(myCar) end
    Log.Print("mod stopped")
end

An error raised inside onUnload is logged and the state is closed anyway, so a broken cleanup cannot wedge the loader.

MOD_DIR

Absolute path of the current mod's folder, with no trailing separator. nil for a buffer launched from the editor.

SCRIPT_NAME

Identifier of the current instance.

There is no onLoad(). Initialisation is simply the code written at file scope: it runs once when the file is loaded.


Mapping from the old API

Before Now
print(...) Console.Print(...)
log(msg) Log.Print(msg)
msgbox(msg) Console.MsgBox(msg)
draw_text(...) Draw.Text(...)
draw_rect(...) Draw.Rect(...)
is_key_down(vk) Input.IsKeyDown(vk)
set_menu_open(b) Input.SetMenuOpen(b)
is_pad_button_down(b) Gamepad.IsButtonDown(b)
call_native(...) Native.Call(...)
call_native_out(...) Native.CallOut(...)
call_native_probe(...) Native.Probe(...)
native_info(n) Native.Info(n)
native_available(n) Native.Available(n)
native_address(n) Native.Address(n)
string_addr(s) Native.StringAddr(s)
module_info([n]) Memory.ModuleInfo([n])
mem_find_string(...) Memory.FindString(...)
mem_find_dword(...) Memory.FindDword(...)
mem_read_hex(...) Memory.ReadHex(...)
mem_write_hex(...) Memory.WriteHex(...)
mem_read_u32(a) Memory.ReadU32(a)
call_thiscall1(...) Memory.CallThiscall1(...)
call_vtable_method0(...) Memory.CallVtableMethod0(...)
resolve_vehicle_ptr(v) Vehicle.ResolvePtr(v)
hide_vehicle_component(...) Vehicle.HideComponent(...)
restore_vehicle_components(v) Vehicle.RestoreComponents(v)
vehicle_bone_prepare(v) Vehicle.BonePrepare(v)
vehicle_has_bone(...) Vehicle.HasBone(...)
set_vehicle_bone_visible(...) Vehicle.SetBoneVisible(...)
is_vehicle_bone_visible(...) Vehicle.IsBoneVisible(...)
vehicle_bone_names(...) Vehicle.BoneNames(...)
declare_mod_file(p) Script.DeclareModFile(p)
is_script_tick_fallback() Script.IsTickFallback()
TimeMs() Script.TimeMs()
CreateThread(fn, ...) Thread.Create(fn, ...)
Wait(ms) Thread.Wait(ms)
StopThread(id) Thread.Stop(id)
ThreadCount() Thread.Count()
SET_CHAR_HEALTH(...) SetCharHealth(...)

The multiplayer compatibility layer (Events, Chat, Player, Game, Console.Log) has been removed. Scripts that relied on it must call natives directly.