AI Core

Players and characters

online, onCharacter, notify, nearby players, positions, and per-player stores: PlayerMap, Cooldowns, KeyedCooldowns, SourceLock, RateLimit, KeyWatchers.

TypeScript
import { online, onCharacter, notify, playersNear, coordsOf, PlayerMap, Cooldowns } from "@aicore-fivem/lib/server";

A player is a connected client with a server id (source). A character is the person they play; a player on the character screen has no character loaded. ai-core owns players and characters; these helpers read them.

online

online is your resource's copy of ai-core's loaded players: characters, job and gang memberships with grades, member lists per job and gang, and duty. It reads ai-core once on first use and then follows ai-core's events, so a read is a memory lookup. Use it in hot paths (every chat message, every alert) instead of calling ai-core's exports.

In Lua, aiLib.online asks ai-core's exports on every call: fine for actions, not for every tick. It has character, name, jobs, gangs, jobGrade, gangGrade, dutyJob and sources; the rest has no Lua twin.

online.sources

The server ids of every player with a loaded character.

Side: Server · Waits: no

Signature
online.sources(): IterableIterator<number>
Lua
local list = aiLib.online.sources() -- integer[]

Returns

IterableIterator<number> (Lua: an array). Players on the character screen are not in it.

Example

import { online, notify } from "@aicore-fivem/lib/server";

for (const source of online.sources()) notify(source, "Server restart in 5 minutes", "warning", 10_000);

online.size

How many players have a loaded character.

Side: Server · Waits: no

Signature
readonly online.size: number

No Lua twin: #aiLib.online.sources().

online.character

The loaded character of a player.

Side: Server · Waits: no

Signature
online.character(source: number): CharacterInfo | null
Lua
local character = aiLib.online.character(source) -- table or nil

Prop

Type

Returns

CharacterInfo (id, citizenid, slot, firstname, lastname, birthdate, sex, nationality), or null (Lua nil) when the player has no character loaded.

Example

const character = online.character(source);
if (character) console.log(`${character.citizenid} is ${character.firstname}`);

online.charId

The id of the player's loaded character (the database id, stable across reconnects).

Side: Server · Waits: no

Signature
online.charId(source: number): number | undefined

No Lua twin: (aiLib.online.character(source) or {}).id.

Returns the character id, or undefined when none is loaded. Key data that must survive a reconnect (cooldowns, ownership) by this id, not by source.

const charId = online.charId(source);
if (charId === undefined) return fail("NO_CHARACTER");

online.name

The character's "Firstname Lastname", or the FiveM player name when no character is loaded.

Side: Server · Waits: no

Signature
online.name(source: number, options?: { fallback?: boolean }): string
Lua
local name = aiLib.online.name(source, fallback) -- fallback: boolean?

Prop

Type

Returns string.

notify(target, `${online.name(source)} sent you a gift`, "success");

online.jobs

Every job the player is a member of, on duty or not. ai-core lets a character hold several jobs (maxJobs in the panel) with one on-duty job at a time.

Side: Server · Waits: no

Signature
online.jobs(source: number): readonly string[]
Lua
local memberships = aiLib.online.jobs(source) -- { { job = 'police', grade = 2 }, ... }

Returns the job names (empty when none or no character). The Lua twin returns membership tables { job, grade }, not names.

const isMechanic = online.jobs(source).includes("mechanic");

online.gangs

Every gang the player is in.

Side: Server · Waits: no

Signature
online.gangs(source: number): readonly string[]
Lua
local memberships = aiLib.online.gangs(source) -- { { gang = 'ballas', grade = 0 }, ... }

Returns the gang names (empty when none). The Lua twin returns membership tables { gang, grade }.

if (online.gangs(source).length === 0) return fail("NOT_IN_GANG");

online.jobGrade

The player's grade in one job.

Side: Server · Waits: no

Signature
online.jobGrade(source: number, job: string): number | null
Lua
local grade = aiLib.online.jobGrade(source, job) -- integer or nil

Prop

Type

Returns the grade number, or null (Lua nil) when the player is not a member. Grade 0 is a member: compare with null, not with a truthy check.

const grade = online.jobGrade(source, "police");
if (grade === null || grade < 3) return fail("RANK_TOO_LOW");

online.gangGrade

The player's grade in one gang.

Side: Server · Waits: no

Signature
online.gangGrade(source: number, gang: string): number | null
Lua
local grade = aiLib.online.gangGrade(source, gang) -- integer or nil

Returns the grade, or null (Lua nil) when not a member.

const boss = (online.gangGrade(source, "ballas") ?? -1) >= 4;

online.jobMembers

Every player with a membership in a job, on duty or not.

Side: Server · Waits: no

Signature
online.jobMembers(job: string): ReadonlySet<number>

No Lua twin.

Returns a live set of server ids (empty when none). It changes as players join and leave the job: copy it ([...set]) before an await.

for (const source of online.jobMembers("mechanic")) notify(source, "A customer is waiting", "info");

online.gangMembers

Every player in a gang.

Side: Server · Waits: no

Signature
online.gangMembers(gang: string): ReadonlySet<number>

No Lua twin. Returns a live set of server ids; copy it before an await.

const members = [...online.gangMembers("ballas")];

online.dutyJob

The job the player is on duty for.

Side: Server · Waits: no

Signature
online.dutyJob(source: number): string | null
Lua
local job = aiLib.online.dutyJob(source) -- string or nil

Returns the job name, or null (Lua nil) when off duty.

if (online.dutyJob(source) !== "ambulance") return fail("NOT_ON_DUTY");

online.onDutyIn

Whether the player is on duty for one of several jobs (all police jobs, all EMS jobs).

Side: Server · Waits: no

Signature
online.onDutyIn(source: number, jobs: readonly string[]): boolean

No Lua twin. Returns true when the on-duty job is in jobs.

const POLICE = ["police", "sheriff"];
if (!online.onDutyIn(source, POLICE)) return fail("NOT_POLICE");

online.onDutyFor

The players on duty for one job.

Side: Server · Waits: no

Signature
online.onDutyFor(job: string): ReadonlySet<number>

No Lua twin. Returns a live set of server ids; copy it before an await.

for (const medic of online.onDutyFor("ambulance")) notify(medic, "Patient at Pillbox", "info");

online.countOnDuty

How many players are on duty over several jobs.

Side: Server · Waits: no

Signature
online.countOnDuty(jobs: readonly string[]): number

No Lua twin. Returns the total count (cost grows with the number of jobs, not players).

if (online.countOnDuty(["police", "sheriff"]) < 2) return fail("NOT_ENOUGH_POLICE");

online.allows

Whether the player may use a place locked by an access rule (jobs, gangs, minimum grades, on-duty). The client's allows uses the same rule, so what a player sees is what the server lets them use.

Side: Server · Waits: no

Signature
online.allows(source: number, rule: AccessRule): boolean

No Lua twin.

Prop

Type

Returns true when the lists are empty, the player is in one of gangs at its minimum grade, or in one of jobs at its minimum grade (and on duty for it when onDuty is set).

import { online } from "@aicore-fivem/lib/server";
import { readAccess, fail } from "@aicore-fivem/lib/shared";

const rule = readAccess(stashRecord); // { jobs: ["police"], gangs: [], grades: { police: 2 }, onDuty: true }
if (!online.allows(source, rule)) return fail("NO_ACCESS");

Character lifecycle

onCharacter

Runs your code when a character enters the world and when it leaves. It also replays the characters already in the world when your resource (re)starts, and covers drops, so you need no playerDropped handler next to it.

Side: Server · Waits: no

Signature
onCharacter(handlers: CharacterHandlers): void
Lua
aiLib.onCharacter({ loaded = function(source, character) end, unloaded = function(source, charId, reason) end })

CharacterHandlers

Prop

Type

Returns

void.

Example

import { onCharacter, PlayerMap } from "@aicore-fivem/lib/server";

const playtime = new PlayerMap<{ charId: number; since: number }>();

onCharacter({
  loaded(source, character) {
    playtime.set(source, { charId: character.id, since: Date.now() });
  },
  unloaded(source, charId, reason) {
    const entry = playtime.get(source);
    if (entry) console.log(`character ${charId} played ${Date.now() - entry.since} ms (${reason})`);
    playtime.delete(source);
  },
});

Call it once at the top level of a server file. The PlayerMap above is emptied on drop by itself; the unloaded handler still runs first for a drop, so you can save data there.

Talking to players

notify

Shows a notification card on one player's screen (ai-lib draws it, or the notification UI the owner picked in ai-core's panel).

Side: Server · Waits: no

Signature
notify(source: number, text: string, kind?: NotificationKind, duration?: number): void
Lua
aiLib.notify(source, text, kind, duration)

Parameters

Prop

Type

Returns

void.

Example

import { notify, createLocale } from "@aicore-fivem/lib/server";

const Locale = createLocale();
notify(source, Locale("PAID", 250), "success");

To answer a client action, return fail("KEY") instead of calling notify: the client's callAction shows the translated error itself.

isOnline

Whether a player is still connected. Check it after a long await before you give anything.

Side: Server · Waits: no

Signature
isOnline(source: number): boolean

No Lua twin: GetPlayerName(source) ~= nil.

Returns true while the player is connected.

await new Promise((resolve) => setTimeout(resolve, 60_000));
if (!isOnline(source)) return;

Nearby players

These ask ai-core which players are in each other's OneSync scope (about 424 metres) and measure only those few, instead of walking every player on the server. A radius larger than about 424 m finds nobody further.

playersNear

The players within radius metres of a player.

Side: Server · Waits: no

Signature
playersNear(source: number, radius: number): number[]
Lua
local list = aiLib.playersNear(source, radius) -- integer[]

Prop

Type

Returns an array of server ids. TypeScript includes source itself; Lua leaves it out.

for (const other of playersNear(source, 20)) if (other !== source) notify(other, "You hear a scream", "warning");

playersNearEntity

The players within radius metres of a networked entity (a vehicle, a prop): its owner and the players in the owner's scope.

Side: Server · Waits: no

Signature
playersNearEntity(entity: number, radius: number): number[]
Lua
local list = aiLib.playersNearEntity(entity, radius) -- integer[]

Prop

Type

Returns an array of server ids. Empty when the entity does not exist or no player owns it (an entity only the server owns has nobody near it).

const witnesses = playersNearEntity(vehicle, 30);

nearestPlayerTo

The closest other player within metres of a player that passes your filter.

Side: Server · Waits: no

Signature
nearestPlayerTo(source: number, metres: number, filter?: (other: number) => boolean): number | null
Lua
local other = aiLib.nearestPlayerTo(source, metres, filter) -- integer or nil

Prop

Type

Returns the server id of the closest match, or null (Lua nil) when none.

const target = nearestPlayerTo(source, 2, (other) => online.character(other) !== null);
if (target === null) return fail("NOBODY_NEAR");

Positions

No Lua twins: Lua has vector math (GetEntityCoords(GetPlayerPed(source)), #(a - b)).

coordsOf

A player's ped position as the server sees it.

Side: Server · Waits: no

Signature
coordsOf(source: number): Vec3

Returns a Vec3 { x, y, z }.

const { x, y, z } = coordsOf(source);

entityCoords

Any entity's position (the server copy of its owner's sync).

Side: Server · Waits: no

Signature
entityCoords(entity: number): Vec3

Returns a Vec3.

import { distance } from "@aicore-fivem/lib/shared";
if (distance(entityCoords(vehicle), coordsOf(source)) > 5) return fail("TOO_FAR");

isNear

Whether the player's ped is within metres of a point. The near guard uses it.

Side: Server · Waits: no

Signature
isNear(source: number, point: Vec3, metres: number): boolean

Prop

Type

Returns true when within reach.

if (!isNear(source, { x: 25.7, y: -1347.3, z: 29.5 }, 3)) return fail("TOO_FAR");

vehicleOf

The vehicle the player sits in.

Side: Server · Waits: no

Signature
vehicleOf(source: number, options?: { driver?: boolean }): number

Prop

Type

Returns the vehicle entity, or 0 on foot (or not in the driver seat with driver: true).

const vehicle = vehicleOf(source, { driver: true });
if (!vehicle) return fail("NOT_DRIVING");

Per-player stores

None of these have a Lua twin: use a Lua table and clear it in playerDropped. Create every store once at module level, never inside a handler.

PlayerMap

A Map keyed by player server id that empties itself when the player drops. Use it for anything you keep per player (an open screen, a selected item).

Side: Server · Waits: no

Signature
new PlayerMap<V>(options?: { onDrop?: (source: number, value: V) => void })

Prop

Type

It has every method of a JavaScript Map (get, set, has, delete, size, iteration).

import { PlayerMap } from "@aicore-fivem/lib/server";

const carrying = new PlayerMap<string>({
  onDrop: (source, item) => console.log(`${source} left while carrying ${item}`),
});
carrying.set(source, "box");

Cooldowns

Per-player cooldowns on the game clock, freed when the player drops.

Side: Server · Waits: no

Signature
new Cooldowns()
ready(source: number): boolean
start(source: number, ms: number): void
take(source: number, ms: number): boolean
left(source: number): number
clear(source: number): void
MethodWhat it doesReturns
ready(source)No cooldown runs for the player.boolean
start(source, ms)Starts (or restarts) the player's cooldown for ms milliseconds.void
take(source, ms)Starts the cooldown when it is ready.true when started, false while one still runs
left(source)Milliseconds left.number, 0 when ready
clear(source)Ends the player's cooldown.void
import { onClientCallback, Cooldowns } from "@aicore-fivem/lib/server";
import { fail } from "@aicore-fivem/lib/shared";

const searchCooldown = new Cooldowns();

onClientCallback("my-bins:search", (source) => {
  if (!searchCooldown.take(source, 30_000)) return fail("WAIT");
  return { ok: true };
});

A player can reconnect to reset a Cooldowns entry. A cooldown that must survive a reconnect uses KeyedCooldowns keyed by online.charId(source).

KeyedCooldowns

Cooldowns by any key that outlives a connection: a character id, an entity, a door id. Expired entries are cleaned up as the map grows.

Side: Server · Waits: no

Signature
new KeyedCooldowns<K = number>()
ready(key: K): boolean
start(key: K, ms: number): void
take(key: K, ms: number): boolean
left(key: K): number
clear(key: K): void

Same methods as Cooldowns, by key. start with ms of 0 or less ends the cooldown.

import { KeyedCooldowns, online } from "@aicore-fivem/lib/server";
import { fail } from "@aicore-fivem/lib/shared";

const robbed = new KeyedCooldowns<string>(); // by store id
const perCharacter = new KeyedCooldowns(); // by character id

function canRob(source: number, store: string) {
  const charId = online.charId(source);
  if (charId === undefined) return fail("NO_CHARACTER");
  if (!robbed.ready(store)) return fail("STORE_EMPTY");
  if (!perCharacter.take(charId, 15 * 60_000)) return fail("WAIT");
  robbed.start(store, 30 * 60_000);
  return { ok: true };
}

SourceLock

One running action per player across several callbacks. A callback's own lock: true covers only that callback; two actions that must exclude each other (list and sell the same vehicle) share one SourceLock.

Side: Server · Waits: no

Signature
new SourceLock()
run<T, B>(source: number, busy: B, task: () => Promise<T>): Promise<T | B>
release(source: number): void

Prop

Type

Returns (run) the task's result, or busy. release(source) frees the player; call it from playerDropped (a task still running keeps its own entry until it settles).

import { onClientCallback, SourceLock } from "@aicore-fivem/lib/server";
import { fail } from "@aicore-fivem/lib/shared";

const dealer = new SourceLock();
on("playerDropped", () => dealer.release(Number(globalThis.source)));

onClientCallback("my-dealer:list", (source) => dealer.run(source, fail("BUSY"), async () => ({ ok: true })));
onClientCallback("my-dealer:sell", (source) => dealer.run(source, fail("BUSY"), async () => ({ ok: true })));

RateLimit

A per-player burst limit: at most count actions inside any windowMs. The gate already rate-limits every input; use this for an extra, slower limit on one action.

Side: Server · Waits: no

Signature
new RateLimit(count: number, windowMs: number)
take(source: number): boolean
release(source: number): void
Lua
local limit = aiLib.RateLimit.new(count, windowMs)
local allowed = limit:take(source)
limit:release(source)

Prop

Type

Returns (take) true and counts one action, or false when the player is over the limit (not counted). release(source) forgets the player; call it from playerDropped.

import { onClientCallback, RateLimit } from "@aicore-fivem/lib/server";
import { fail } from "@aicore-fivem/lib/shared";

const reports = new RateLimit(3, 60_000);
on("playerDropped", () => reports.release(Number(globalThis.source)));

onClientCallback("my-report:send", (source) => {
  if (!reports.take(source)) return fail("TOO_MANY_REPORTS");
  return { ok: true };
});

KeyWatchers

The players who receive live changes of one key (a parking lot's cars, an open character sheet). One key per player: watching a new key stops the old one. Freed on drop; every operation is constant time.

Side: Server · Waits: no

Signature
new KeyWatchers<K>()
watch(source: number, key: K): void
stop(source: number): void
of(key: K): ReadonlySet<number> | undefined
keyOf(source: number): K | undefined
MethodWhat it doesReturns
watch(source, key)The player watches key and stops watching the one before.void
stop(source)The player stops watching.void
of(key)The players watching key.a set, or undefined when nobody watches
keyOf(source)The key the player watches.the key, or undefined
import { onClientCallback, onClientEvent, KeyWatchers } from "@aicore-fivem/lib/server";
import { oneArg, readText } from "@aicore-fivem/lib/shared";

const lots = new KeyWatchers<string>();

onClientCallback("my-lot:open", { args: oneArg((v) => readText(v, 32)) }, (source, lot) => {
  lots.watch(source, lot);
  return { ok: true, cars: [] }; // the snapshot
});
onClientEvent("my-lot:close", (source) => lots.stop(source));

function carChanged(lot: string, car: unknown) {
  for (const source of lots.of(lot) ?? []) emitNet("my-lot:car", source, car); // the delta
}

On this page