Every docs URL answers with Markdown when you add .md. Site map: llms.txt
Lua, JavaScript and C#: one script, three runtimes
FiveM runs three scripting runtimes. What a native call, a wait loop, a hash, a vector and an event look like in each, and where Lua has no clean equivalent.
Esta documentación está en inglés por ahora.
FiveM ships three scripting runtimes and a resource can mix them: Lua, JavaScript on V8 (TypeScript compiles down to it) and C# on Mono. They all call the same natives under the same names, so most of a script looks the same in all three. The differences are in the ceremony around it: how you wait, how you hash a name, what a vector is, and where the code lives.
Every snippet block on this site has a Lua / JS / TS / C# switcher in its top left corner. Pick a runtime once and every snippet on every page follows it, so this page is the reference for what the switcher is doing.
Calling a native
All three runtimes expose natives as PascalCase functions with the same name, generated from the same native list:
local ped = PlayerPedId()
SetEntityCoords(ped, 674.883, 548.269, 133.94, false, false, false, true)const ped = PlayerPedId();
SetEntityCoords(ped, 674.883, 548.269, 133.94, false, false, false, true);using CitizenFX.Core;
using static CitizenFX.Core.Native.API;
int ped = PlayerPedId();
SetEntityCoords(ped, 674.883f, 548.269f, 133.94f, false, false, false, true);Note
In C# the natives live in CitizenFX.Core.Native.API. The using static line is what lets you write SetEntityCoords(...) instead of API.SetEntityCoords(...). Float parameters need the f suffix.
There is a second, lower level C# form, Function.Call(Hash.SET_ENTITY_COORDS, ...), with the raw SCREAMING_SNAKE native name as the enum member. It exists, but no official example uses it: the API wrappers are the documented way.
Where the code lives
Lua and JavaScript run their top level code when the resource starts. C# has no top level code at all: a script is a class that derives from BaseScript, and its constructor is the “when the resource starts” hook.
public class MyScript : BaseScript
{
public MyScript()
{
// runs when the resource starts
}
}Waiting
This is the biggest difference.
| How you yield | |
|---|---|
| Lua | Wait(0) anywhere, including at the top level |
| JavaScript | no built-in wait; write const Delay = (ms) => new Promise((res) => setTimeout(res, ms)); and await it |
| C# | await Delay(0), which BaseScript provides, inside an async Task method |
RequestModel(`adder`)
while not HasModelLoaded(`adder`) do Wait(0) endconst Delay = (ms) => new Promise((res) => setTimeout(res, ms));
(async () => {
const model = GetHashKey("adder");
RequestModel(model);
while (!HasModelLoaded(model)) await Delay(0);
})();The IIFE is there because top level await is not available in the V8 runtime.
public class SpawnCar : BaseScript
{
public SpawnCar()
{
Tick += Run;
}
private async Task Run()
{
Tick -= Run; // run this once, not every frame
var model = (uint)GetHashKey("adder");
RequestModel(model);
while (!HasModelLoaded(model)) await Delay(0);
}
}A Tick handler is the only place a BaseScript gets an async method, so a one-off task hooks Tick, unhooks itself on the first frame and carries on.
Running every frame
Lua’s CreateThread runs its body once. The per frame loop is the while true do Wait(0) end inside it. The other two runtimes have a real per frame hook, so there is no loop to write:
CreateThread(function()
while true do
Wait(0)
DrawMarker(1, x, y, z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 255, 0, 0, 200, false, false, 2, false, nil, nil, false)
end
end)setTick(async () => {
DrawMarker(1, x, y, z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 255, 0, 0, 200, false, false, 2, false, null, null, false);
});public MyScript()
{
Tick += OnTick;
}
private async Task OnTick()
{
DrawMarker(1, x, y, z, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 255, 0, 0, 200, false, false, 2, false, null, null, false);
}Tip
If a setTick callback returns a promise, the runtime skips that ticker until the promise resolves, so an async body never stacks up.
Hashes
Lua has a compile time hash literal. Nothing else does.
local model = `prop_bench_01a`const model = GetHashKey("prop_bench_01a");var model = (uint)GetHashKey("prop_bench_01a");Important
The C# cast is not cosmetic. GetHashKey returns an int while every native parameter typed Hash takes a uint, so without (uint) it is a compile error.
In JavaScript you can also pass the name straight through wherever the parameter is a Hash, because the generated wrapper hashes a string for you. GetHashKey is spelled out here because it works everywhere, including parameters typed plain int.
Vectors
A native that returns a vector gives you three different things:
local coords = GetEntityCoords(ped, false)
print(coords.x, coords.y, coords.z)const coords = GetEntityCoords(ped, false); // a plain [x, y, z] array
console.log(coords[0], coords[1], coords[2]);Vector3 coords = GetEntityCoords(ped, false);
Debug.WriteLine($"{coords.X} {coords.Y} {coords.Z}");Lua also has vector arithmetic, so #(a - b) is the distance between two points. The other runtimes have no vector type to subtract, so use the native instead:
const away = GetDistanceBetweenCoords(a[0], a[1], a[2], b[0], b[1], b[2], true);Natives with pointer outputs
Some natives hand values back through pointers. Lua and JavaScript both turn those into extra return values; C# takes them as ref parameters, in their real position in the argument list, and a ref argument has to be assigned first.
local ok, value = StatGetInt(stat, -1)const [ok, value] = StatGetInt(stat, -1);int value = 0;
var ok = StatGetInt(stat, ref value, -1);Printing
print is Lua only.
| Lua | JavaScript | C# |
|---|---|---|
print(a, b) |
console.log(a, b) |
Debug.WriteLine($"{a} {b}") |
Commands and events
RegisterCommand("car", function(source, args, rawCommand)
-- ...
end, false)
RegisterNetEvent("myEvent")
AddEventHandler("myEvent", function(a, b) end)RegisterCommand("car", async (source, args, raw) => {
// ...
}, false);
onNet("myEvent", (a, b) => {}); // net event, registers and handles in one call
on("myEvent", (a, b) => {}); // local eventpublic MyScript()
{
RegisterCommand("car", new Action<int, List<object>, string>(async (source, args, raw) =>
{
// ...
}), false);
EventHandlers["myEvent"] += new Action<string, bool>(OnMyEvent);
}C# needs no RegisterNetEvent: handling a net event registers it.
The manifest
fx_version 'cerulean'
game 'gta5'
client_script 'client.lua'
client_script 'client.js'
client_script 'MyResource.Client.net.dll'The loader picks the runtime from the file extension: .lua, .js and .net.dll.
Warning
There is no .ts handler. TypeScript has to be compiled to .js and the manifest points at the build output. The official typings are the @citizenfx/client and @citizenfx/server npm packages; add the matching one to types in your tsconfig for the side you are writing.
Where each runtime has no clean equivalent
- Backtick hashes. Lua only. Everywhere else it is a
GetHashKeycall at run time, per call. - Multiple return values. Lua returns them loose, JavaScript packs them into an array, C# uses
refparameters. Waitat the top level. Lua only. JavaScript needs an async wrapper, C# needs a method.- Varargs and table syntax. A Lua table is an object literal in JavaScript and an anonymous object (
new { ... }) or aDictionary<,>in C#, depending on whether the keys are members or data. TriggerClientEvent. Lua and JavaScript take the server id as the second argument; C# takes aPlayerfirst, or drops it entirely to reach everyone.source. A global during the handler in Lua and JavaScript; in C# it is a[FromSource] Playerparameter instead.
Sources
Everything on this page is from the official material, not from memory: the JavaScript runtime and C# runtime pages, Creating your first script in C#, the resource manifest reference, and the shipped @citizenfx/client typings, which are where the exact argument lists and return shapes above come from.
