Players and characters
online, onCharacter, notify, nearby players, positions, and per-player stores: PlayerMap, Cooldowns, KeyedCooldowns, SourceLock, RateLimit, KeyWatchers.
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
online.sources(): IterableIterator<number>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
readonly online.size: numberNo Lua twin: #aiLib.online.sources().
online.character
The loaded character of a player.
Side: Server · Waits: no
online.character(source: number): CharacterInfo | nulllocal character = aiLib.online.character(source) -- table or nilProp
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
online.charId(source: number): number | undefinedNo 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
online.name(source: number, options?: { fallback?: boolean }): stringlocal 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
online.jobs(source: number): readonly string[]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
online.gangs(source: number): readonly string[]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
online.jobGrade(source: number, job: string): number | nulllocal grade = aiLib.online.jobGrade(source, job) -- integer or nilProp
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
online.gangGrade(source: number, gang: string): number | nulllocal grade = aiLib.online.gangGrade(source, gang) -- integer or nilReturns 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
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
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
online.dutyJob(source: number): string | nulllocal job = aiLib.online.dutyJob(source) -- string or nilReturns 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
online.onDutyIn(source: number, jobs: readonly string[]): booleanNo 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
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
online.countOnDuty(jobs: readonly string[]): numberNo 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
online.allows(source: number, rule: AccessRule): booleanNo 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
onCharacter(handlers: CharacterHandlers): voidaiLib.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
notify(source: number, text: string, kind?: NotificationKind, duration?: number): voidaiLib.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
isOnline(source: number): booleanNo 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
playersNear(source: number, radius: number): number[]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
playersNearEntity(entity: number, radius: number): number[]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
nearestPlayerTo(source: number, metres: number, filter?: (other: number) => boolean): number | nulllocal other = aiLib.nearestPlayerTo(source, metres, filter) -- integer or nilProp
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
coordsOf(source: number): Vec3Returns 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
entityCoords(entity: number): Vec3Returns 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
isNear(source: number, point: Vec3, metres: number): booleanProp
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
vehicleOf(source: number, options?: { driver?: boolean }): numberProp
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
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
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| Method | What it does | Returns |
|---|---|---|
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
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): voidSame 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
new SourceLock()
run<T, B>(source: number, busy: B, task: () => Promise<T>): Promise<T | B>
release(source: number): voidProp
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
new RateLimit(count: number, windowMs: number)
take(source: number): boolean
release(source: number): voidlocal 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
new KeyWatchers<K>()
watch(source: number, key: K): void
stop(source: number): void
of(key: K): ReadonlySet<number> | undefined
keyOf(source: number): K | undefined| Method | What it does | Returns |
|---|---|---|
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
}