These rules come from the problems we hit while building and running JRS Core. Follow them in your own scripts and your server will stay smooth.
Performance#
Sleep while nothing happens. A loop with Wait(0) runs every frame. Use it only while something visible needs it.
CreateThread(function()
while true do
local sleep = 1000 -- idle: check once per second
local dist = #(GetEntityCoords(PlayerPedId()) - spot)
if dist < 5.0 then
sleep = 0 -- close: react every frame
DrawPrompt()
end
Wait(sleep)
end
end)CreateThread(function()
while true do
Wait(0) -- always every frame
if #(GetEntityCoords(PlayerPedId()) - spot) < 5.0 then DrawPrompt() end
end
end)Cache what you reuse. PlayerPedId() and GetEntityCoords() are natives: call them once per loop turn, not once per check.
Do not create garbage in hot loops. New tables and string concatenations inside a per-frame loop make the garbage collector work all the time. Build them once outside the loop.
Tables and arrays#
Tables with named keys are looked up directly. A list has to be searched one element at a time, so use a table as a set for membership tests:
local police = { police = true, sheriff = true }
local isLaw = police[job] -- one lookuplocal police = { 'police', 'sheriff' }
local isLaw = false
for i = 1, #police do
if police[i] == job then isLaw = true break end
endUse named fields for structured data ({ grade = 0, job = 'police' }) instead of positional ones ({ 0, 'police' }): they read better and survive changes.
Security#
Everything a client sends can be forged. The server decides.
Never accept a price, amount, reward, item name or coordinates from the client as the truth. Look them up on the server.
RegisterNetEvent('myres:buy', function(item, count)
local src = source
count = math.floor(tonumber(count) or 0)
local def = Config.Items[item] -- price comes from the server config
if not def or count < 1 or count > 20 then return end
local ped = GetPlayerPed(src)
if #(GetEntityCoords(ped) - Config.Shop.coords) > 6.0 then return end -- distance check
local JRS = exports.jrs_core:getCoreApi()
if not JRS.RemoveMoney(src, def.price * count) then return end -- pay first
exports.jrs_inventory:addItem(src, item, count)
end)- Add a cooldown per player to every event that gives money or items.
- Check permission (job, group, level) on the server, never only in the menu.
- Give rewards with
JRS.AddReward(...)so the economy multipliers apply. - Do not send secrets (webhooks, API keys) to the client or put them in a
sharedfile.
Database#
Always use parameters. Never build SQL by joining strings.
local rows = MySQL.query.await('SELECT * FROM jrs_items WHERE item = ?', { item })local rows = MySQL.query.await("SELECT * FROM jrs_items WHERE item = '" .. item .. "'")- Call
MySQL.*.awaitinside a thread or an event handler, not at file load. - Select only the columns you need and add an index for the column you filter on.
- Save in batches (on interval and on drop), not on every small change.
Structure#
- One resource, one job. Put shared tables in
config.luaand texts inlocales. - Name events
resource:action(jrs_mining:sell) so they never collide. - Prefix every
printwith your resource name and hide debug output behindConfig.Debug. - Keep numbers readable: write hashes and flag ids as named constants with a comment.
local FLAG = { NO_RAGDOLL = 0 } -- id found by testing on build 1491Clean up after yourself#
Everything you create must be removed when the resource stops, or players keep ghost props, blips and prompts after a restart.
AddEventHandler('onResourceStop', function(res)
if res ~= GetCurrentResourceName() then return end
for _, blip in ipairs(blips) do RemoveBlip(blip) end
for _, obj in ipairs(props) do DeleteEntity(obj) end
SetNuiFocus(false, false)
end)Release assets too: RemoveAnimDict, SetModelAsNoLongerNeeded, SetStreamedTextureDictAsNoLongerNeeded.
NUI (web interfaces)#
- Show player text with
textContent, neverinnerHTML, so a name cannot inject code. - Always give a way out (Esc and a close button) and call
SetNuiFocus(false, false)when you close. - Keep the page small: one script file, no external CDN (the browser inside the game may be offline or blocked).
Handle failures#
Wrap code that depends on outside input (JSON, database rows, another resource) in pcall and log the error. Do not hide your own bugs with it.
local ok, data = pcall(json.decode, raw)
if not ok or type(data) ~= 'table' then
print(('[myres] bad data: %s'):format(tostring(data)))
return
endBefore you release#
- Restart the resource twice and check the console is clean.
- Test with two players: one doing the action, one watching.
- Try to break your own events: wrong item, huge count, far away, spamming.
- Check the idle cost with the resource monitor (
resmonin F8): an idle script should stay near 0.00 ms.