Lua API Reference¶
The loader's API comes down to two rules.
1. GTA IV natives are global functions, in PascalCase.
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.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 globalWaitfunction. That is one reason for the namespace:Thread.Waitcan no longer collide with it. The native stays reachable viaNative.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.
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. Default0xFFFFFFFF(opaque white).size(int, optional): glyph height in pixels. Omitted or0: the system font, exactly as before.font(string, optional): a GDI family name ("Segoe UI","Impact", or a family loaded withDraw.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. Default0xFF000000(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. Pass0for both to draw at the file's native size, or0for one of them to keep the aspect ratio.path(string): e.g."ui/button.png".tint(int, optional):0xAARRGGBBmultiplied into the image. Default0xFFFFFFFF(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
ScriptAnyby public sources, so they are registered asint: the typed global would silently cast a string or a float to an integer.Native.Callpushes 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.
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.
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.