Typed Haxe externs for the complete FiveM/RedM native API — generated straight from CitizenFX's own native databases — plus an object-oriented core layer for entities, players, zones, callbacks and UI. Compiles to plain Lua that runs on an unmodified server.
A speedometer. First, what you write in Lua today — then the same thing with fivem-hx.
-- Lua
Citizen.CreateThread(function()
while true do
Citizen.Wait(0)
local ped = PlayerPedId()
local veh = GetVehiclePedIsIn(ped, false)
if veh ~= 0 then
SetTextFont(4)
SetTextScale(0.45, 0.45)
SetTextColour(255, 255, 255, 255)
SetTextOutline()
SetTextEntry("STRING")
AddTextComponentString(math.floor(GetEntitySpeed(veh) * 3.6) .. " km/h")
DrawText(0.5, 0.9)
end
end
end)// Haxe
Thread.everyFrame(() -> {
var vehicle = LocalPlayer.vehicle();
if (vehicle != null)
Ui.drawText('${Math.round(vehicle.speed * 3.6)} km/h', new Vector2(0.5, 0.9));
});Shorter is nice. Not shipping a typo to production is nicer — and that's the actual pitch.
Lua is a fine language. The problem isn't Lua — it's that FiveM's Lua runtime has no idea what the natives are. Every native is a global lookup resolved at call time, every argument is untyped, and every mistake is a runtime surprise on a full server.
Haxe knows every one of them at compile time.
-- Lua: a completely valid program. It just doesn't work.
local ped = PlayerPedId()
local coords = GetEntityCoors(ped) -- typo — this is nil
SetEntityCoords(ped, coords.x, coords.y, coords.z + 10.0)Lua happily loads this resource. Nothing complains until a player triggers the code path,
and then you get attempt to index a nil value (local 'coords') in a console nobody
was watching.
// Haxe: the compiler stops you.
var ped = LocalPlayer.ped();
var coords = ped.coors;
// ^^^^^ Ped has no field coors (Suggestion: coords)The build fails. Your editor underlines it while you type. The bad resource never reaches the server.
SET_ENTITY_COORDS takes eight parameters. Lua doesn't care if you pass three.
-- Lua: missing arguments quietly become nil
SetEntityCoords(ped, 215.9, -810.1) -- forgot z, and the four flags// Haxe: the type system won't let the call compile
ped.coords = new Vector3(215.9, -810.1);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ Not enough arguments, expected z:FloatSame for types: pass a String where a native wants a Float, or a Bool where it
wants an entity handle, and you find out at build time — not from a bug report.
-- Lua: what is -1? what is 4? good luck.
TaskWarpPedIntoVehicle(ped, veh, -1)
SetVehicleDoorsLocked(veh, 4)// Haxe: enums, with autocomplete and documentation on hover
ped.warpIntoVehicle(vehicle, Driver);
vehicle.lockState = LockedPlayerInside; // "Players can't get in, and anyone inside can't get out."VehicleSeat, VehicleLockState, VehicleDoor, EntityType, Control and the rest are
enum abstracts — they're plain integers at runtime with zero overhead, but the
compiler rejects the values that don't exist and your editor lists the ones that do.
-- Lua: spawn a car three metres in front of a ped
local pos = GetEntityCoords(ped)
local fwd = GetEntityForwardVector(ped)
local spawn = vector3(pos.x + fwd.x * 3.0, pos.y + fwd.y * 3.0, pos.z + fwd.z * 3.0)
local dist = #(pos - GetEntityCoords(target))// Haxe: operator overloading, fully inlined
var spawn = ped.coords + ped.forwardVector * 3.0;
var dist = ped.distanceToEntity(target);Vector2/Vector3/Vector4/Quaternion/Rgba are inline abstracts with
+ - * / == -, distance, lerp, normalized, cross, clampLength and friends.
They compile down to the same arithmetic you'd have written by hand.
Asking the server a question in plain Lua means inventing two event names, wiring both directions, and hoping the reply arrives:
-- Lua, client
RegisterNetEvent("garage:spawned")
AddEventHandler("garage:spawned", function(netId)
-- ...and now continue the logic you started somewhere above
end)
TriggerServerEvent("garage:spawn", "adder")
-- Lua, server
RegisterNetEvent("garage:spawn")
AddEventHandler("garage:spawn", function(model)
local src = source
-- ...
TriggerClientEvent("garage:spawned", src, netId)
end)// Haxe, client — one line, and the code stays in one place
var netId:Int = Callbacks.await("garage:spawn", "adder");
// Haxe, server — just return the answer
Callbacks.register("garage:spawn", (source, args) -> {
var player = Player.fromSource(source);
if (player == null) return 0;
var vehicle = Vehicle.create(args[0], player.coords, player.ped.heading);
return vehicle.netId;
});Under the hood it's a correlation key layered over FiveM's own events — no external
dependency — and it works client→server and server→client. await suspends only the
calling coroutine (a thread, event handler, command or export), so the rest of the
resource keeps running while it waits.
FiveM's shared_script gives you one Lua file loaded into two runtimes. It does not
give you any guarantee that the two sides agree about what's in it. So the model
whitelist gets copy-pasted into the client for the UI, drifts from the server's copy,
and now players see cars they can't spawn.
// src/Shared/Vehicles.hx — compiled into BOTH client.lua and server.lua
class Vehicles {
public static final allowed = ["adder", "zentorno", "t20"];
public static function isAllowed(model:String):Bool
return allowed.indexOf(model.toLowerCase()) != -1;
}// Client — same function, same list
if (!Vehicles.isAllowed(model)) { Ui.notify("~r~Not on the list."); return; }
// Server — same function, same list, and it's still the authority
if (!Vehicles.isAllowed(model)) return 0;Rename isAllowed, change its signature, drop a model from the list — both builds fail
or both builds update. One side can never quietly fall behind the other.
| Lua | Haxe | |
|---|---|---|
| Rename a function | grep, pray | compiler finds every call site |
| "What does this return?" | read the wiki, guess | hover in your editor |
| Autocomplete on natives | none | every native, with its docs inline |
nil handling |
if x ~= nil and x ~= 0 everywhere |
Null<T>, ?., and null-safety checks |
| Structured data | untyped tables | typedefs, enums, generics, real classes |
| Dead code / unused imports | ships to the server | flagged at build time |
| Refactoring across files | manual | safe by construction |
This is the part people expect a catch on. There isn't one. Natives are extern
declarations mapped onto FiveM's Lua globals, so the compiler emits the call you would
have written by hand.
Straight from examples/basic-resource — Haxe in, Lua out:
// src/Client/ClientMain.hx
CoreEvents.triggerServerEvent("hx_resource:server_ping", "Hello from client");-- dist/client/client.lua, generated
_G.TriggerServerEvent("hx_resource:server_ping", "Hello from client")No wrapper table, no dispatch layer, no reflection. The math types (Vector3, Rgba,
Hash, the enum abstracts) are abstracts over plain data with inline methods, so
they compile away entirely; the entity wrappers are thin objects around the raw handle
you'd have been passing around anyway. You only pay for what you call.
Worth saying plainly: if you're writing a 20-line resource, editing files live on a
running server, or pasting a snippet off the forums to try in the next five minutes,
Lua's lack of a build step wins. fivem-hx pays off when a resource gets big enough
that you stop being able to hold all of it in your head — which, for most gamemodes,
is somewhere around week three.
haxelib install fivem-hxWorking on the library and a resource at the same time? Use haxelib dev instead — see
docs/getting-started.md.
Requirements: Haxe 4.3+, a FiveM or RedM server to run against, and Python 3.9+ only if you want to regenerate the natives yourself.
// src/Client/ClientMain.hx
import fivem.client.Core;
class ClientMain {
static function main() {
Commands.register("whereami", (_, _) -> {
var coords = LocalPlayer.coords();
Ui.notify('You are at ${coords.toString()}');
});
}
}# client.hxml
-lib fivem-hx
-p src
-main ClientMain
-lua dist/client/client.luahaxe client.hxmlPoint your fxmanifest.lua at dist/client/client.lua and you're running. The full
walkthrough — manifest, server side, build layout — is in
docs/getting-started.md.
A single import fivem.client.Core; (or fivem.server.Core) brings the whole core layer
into scope. Every name in it is a type alias, so the import costs nothing.
- Every native, typed — the complete GTA and CitizenFX native surface for client,
server and shared, regenerated on demand from the live FiveM docs (
generate.py). - An object-oriented core layer —
Entity,Ped,Vehicle,Player,Players,World,Zones,Blip,Ui,Nui,Controls,Raycast,Streaming,Thread,Events,Callbacks,StateBag,Kvp,Exports,Logger, and the math types. See docs/core-api.md. - Correct apiset separation — client-only, server-only and shared natives kept apart, with shared natives merged into both sides so nothing is unreachable (why).
- Multi-return natives that actually return everything — FiveM's Lua runtime appends
out-parameters to the return values;
NativeOutputsmodels that with@:multiReturnso results like ground-Z and shape tests aren't silently thrown away. - Zone system — circles, spheres, boxes and polygons with
onEnter/onExit/onInsidecallbacks, on both client and server — all zones share one adaptive poll loop instead of one thread each. - Database driver —
fivem.server.dbwraps oxmysql as asys.db.Connection, usable directly or with therecord-macrosORM (docs). - Working example resources — not snippets: they compile, and they run.
| Example | What it shows |
|---|---|
examples/core-demo |
The core API end to end — zones, blips, keybinds, server-authoritative vehicle spawning through callbacks, the player registry, state bags |
examples/basic-resource |
The smallest complete client+server resource: native calls, commands, an event across the boundary |
examples/mysql-example |
The oxmysql driver: schema creation, loading and saving a row through a plain Haxe model |
fivem-hx/
├── src/fivem/
│ ├── client/ package fivem.client — client-only + shared natives
│ │ └── core/ Entity, Ped, Vehicle, Player, Zones, Ui, Raycast, ...
│ ├── server/ package fivem.server — server-only + shared natives
│ │ ├── core/ Entity, Ped, Vehicle, Player, Players, Zones, Http, ...
│ │ └── db/ oxmysql-backed sys.db.Connection driver
│ └── shared/ package fivem.shared — shared natives + hand-written externs
│ ├── core/ Thread, Events, Commands, StateBag, Kvp, Resource, ...
│ ├── util/ Vector2/3/4, Quaternion, Rgba, Hash, MathUtil, Logger, ...
│ └── colshape/ Sphere, Circle, Box, Poly
├── examples/ basic-resource, core-demo, mysql-example
├── docs/ in-depth guides (see below)
├── scripts/
│ └── Publish-Docs.ps1 builds the Dox API site and pushes it to the docs branch
├── .github/workflows/ CI (typecheck + build example) and tag-triggered releases
├── generate.py regenerates src/fivem/*/natives from FiveM's docs
├── test-*.hxml type-check-only builds for each package
├── CONTRIBUTING.md
├── CHANGELOG.md
└── haxelib.json
One classpath, one package tree, mirroring how the Haxe standard library is organized —
haxelib install and go, no juggling three separate packages.
docs/architecture.md has the full rationale.
The generated API reference covers the
what — every class and method signature under fivem.client, fivem.server and
fivem.shared, browsable and searchable. The guides below cover the how.
| Guide | What's in it |
|---|---|
| Getting Started | Install, wire up your first resource |
| Core API | The OOP layer: entities, players, vehicles, zones, vectors, callbacks, state bags |
| API Guide | Calling conventions, player source types, vectors, multi-return natives |
| Architecture | Repo layout rationale, apiset merging, how externs compile to zero-cost calls |
| Database (oxmysql) | The sys.db.Connection driver, direct queries, wiring up record-macros |
| Haxe + FiveM Tips | Lua runtime versions, debugging, structuring client/server/shared code |
| Dev Experience | Editor setup, fast builds, watch loops, debugging without source maps |
| Regenerating Natives | How generate.py works, its limitations, how to extend it |
| Versioning | What's PATCH/MINOR/MAJOR when most of the API is generated |
| Publishing | Release checklist, tag-triggered packaging, haxelib submit |
| Troubleshooting | Common compiler and runtime errors, explained |
See also CONTRIBUTING.md and CHANGELOG.md.
Type-check every package — no build target needed, the fastest way to confirm a
change to generate.py or a manual edit under src/ didn't break anything:
haxe test-shared.hxml && haxe test-server.hxml && haxe test-client.hxmlRegenerate the natives — downloads the live FiveM/RedM native databases and rewrites
everything under src/fivem/*/natives/ plus the Natives.hx entry points. Hand-written
files are left alone:
python generate.pyDetails in docs/regenerating-natives.md.
Issues and PRs welcome at github.com/Kleppinger/fivem-hx. CONTRIBUTING.md covers dev setup, the PR checklist, and where a given fix actually belongs (generator vs. generated output vs. hand-written externs) — that last part matters more here than in a typical repo. Every push and PR runs CI: type-check all three packages, build both example resources for client and server, and verify the generated Lua parses.
Follows SemVer, scoped for a mostly-generated externs library in docs/versioning.md — notably, a native's signature changing upstream is a MAJOR change even when it only comes from re-running the generator. Release history lives in CHANGELOG.md.
Maintainers cutting a release: docs/publishing.md. Pushing a
vX.Y.Z tag runs the release workflow, which verifies
the tag matches haxelib.json, type-checks, and attaches a release zip to the GitHub
Release. haxelib submit stays a manual last step — haxelib's CLI has no
non-interactive auth flow to script around.