Threads, waits and timers, wrapped around FiveM's Citizen scheduler.

FiveM has no preemption: a resource runs on a single Lua state, and a "thread" is a coroutine that voluntarily yields. Everything here is built on that model, so no locking or synchronisation is ever needed — but a loop that forgets to wait will freeze the whole resource.

Thread.create(() -> {
    while (true) {
        Thread.wait(1000);
        checkSomething();
    }
});

var ticker = Thread.setInterval(500, () -> updateHud());
ticker.cancel();

Static methods

staticadaptiveLoop(body:() ‑> Int):TimerHandle

Runs body repeatedly, waiting the number of milliseconds it returns between runs. Returning a negative value stops the loop.

This is the pattern for distance-based polling, where you want to check often when the player is close and rarely when they're far:

Thread.adaptiveLoop(() -> {
    var distance = LocalPlayer.coords().distance(shopCoords);
    if (distance > 50) return 1000;
    drawShopMarker();
    return 0;
});

staticinlinecreate(body:() ‑> Void):Void

Runs body on a new coroutine, starting on the next tick.

This is the FiveM equivalent of "go do this in the background" — the caller returns immediately.

staticinlinecreateNow(body:() ‑> Void):Void

Like create, but starts body immediately (synchronously, up to its first wait) instead of deferring it to the next tick.

staticinlinedefer(body:() ‑> Void):Void

Runs body on a fresh coroutine on the next tick, without blocking the caller.

staticeveryFrame(body:() ‑> Void):TimerHandle

Runs body once per frame until cancelled.

Per-frame work is the most expensive thing a resource can do — reserve this for drawing (markers, text, outlines), which genuinely has to happen every frame, and use setInterval for everything else.

staticinlinenextFrame():Void

Yields until the next frame. Shorthand for wait(0).

staticsetInterval(ms:Int, body:() ‑> Void):TimerHandle

Runs body every ms milliseconds until cancelled.

The delay is measured between runs, not on a fixed schedule: a body that takes longer than ms delays the next run rather than stacking up.

staticsetTimeout(ms:Int, body:() ‑> Void):TimerHandle

Runs body once after ms milliseconds. Cancel via the returned handle to prevent it from firing.

@:value({ ms : 0 })staticinlinewait(ms:Int = 0):Void

Yields for at least ms milliseconds. 0 waits for the next frame.

Must be called from inside a coroutine — a thread, event handler, command handler or export. That covers nearly all resource code, but not the top level of your main().

@:value({ pollMs : 0, timeoutMs : 5000 })staticwaitUntil(condition:() ‑> Bool, timeoutMs:Int = 5000, pollMs:Int = 0):Bool

Blocks the current coroutine until condition returns true, polling every pollMs. Returns true if the condition was met, false if timeoutMs elapsed first.

Pass a timeoutMs of 0 to wait forever — but be aware that a condition which never becomes true then leaks a coroutine for the resource's lifetime.