İçeriğe geç
Lua, JavaScript and C#: one script, three runtimes

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.

Bu dokümanlar şimdilik İngilizce.

Last updated

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:

Lua
local ped = PlayerPedId()
SetEntityCoords(ped, 674.883, 548.269, 133.94, false, false, false, true)
JavaScript
const ped = PlayerPedId();
SetEntityCoords(ped, 674.883, 548.269, 133.94, false, false, false, true);
C#
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.

C#
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
Lua
RequestModel(`adder`)
while not HasModelLoaded(`adder`) do Wait(0) end
JavaScript
const 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.

C#
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:

Lua
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)
JavaScript
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);
});
C#
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.

Lua
local model = `prop_bench_01a`
JavaScript
const model = GetHashKey("prop_bench_01a");
C#
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:

Lua
local coords = GetEntityCoords(ped, false)
print(coords.x, coords.y, coords.z)
JavaScript
const coords = GetEntityCoords(ped, false); // a plain [x, y, z] array
console.log(coords[0], coords[1], coords[2]);
C#
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:

JavaScript
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.

Lua
local ok, value = StatGetInt(stat, -1)
JavaScript
const [ok, value] = StatGetInt(stat, -1);
C#
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

Lua
RegisterCommand("car", function(source, args, rawCommand)
    -- ...
end, false)

RegisterNetEvent("myEvent")
AddEventHandler("myEvent", function(a, b) end)
JavaScript
RegisterCommand("car", async (source, args, raw) => {
    // ...
}, false);

onNet("myEvent", (a, b) => {});   // net event, registers and handles in one call
on("myEvent", (a, b) => {});      // local event
C#
public 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

fxmanifest.lua
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 GetHashKey call at run time, per call.
  • Multiple return values. Lua returns them loose, JavaScript packs them into an array, C# uses ref parameters.
  • Wait at 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 a Dictionary<,> 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 a Player first, or drops it entirely to reach everyone.
  • source. A global during the handler in Lua and JavaScript; in C# it is a [FromSource] Player parameter 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.