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.
الأداء
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.