-- server.lua
-- QBCore + ESX compatibility examples included. Adapt to your economy / database if needed.
local QBCore, ESX = nil, nil
local resourceName = GetCurrentResourceName()
-- try to get frameworks
if GetResourceState('qb-core') == 'started' then
QBCore = exports['qb-core']:GetCoreObject()
end
if GetResourceState('es_extended') == 'started' then
TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
end
-- CONFIG fallback (assumes you have a shared Config table in the resource)
local Config = Config or {}
-- Ensure data folder exists (no-op on many hosts, but good to know)
local dataFile = 'data/speedtraps.json'
-- Server state
local SpeedTraps = {}
local CuffedPlayers = {} -- keyed by serverId: {type='hard'|'soft', by=policeId, ts=os.time()}
local OfficerGPS = {} -- keyed by serverId = true (who has gps sharing enabled)
local OnlinePoliceCount = 0
local GSRPlayers = {} -- keyed by serverId = true/false
local OnDutyPlayers = {} -- keyed by serverId = {job=..,grade=..}
local WeaponLicenses = {} -- basic server-side table: WeaponLicenses[targetId] = true
-- -------------------------
-- Persistence helpers
-- -------------------------
local function loadSpeedTraps()
local raw = LoadResourceFile(resourceName, dataFile)
if raw and raw ~= '' then
local ok, decoded = pcall(json.decode, raw)
if ok and type(decoded) == 'table' then
SpeedTraps = decoded
print(("[wasabi_police] Loaded %d speedtraps"):format(#SpeedTraps))
return
end
end
SpeedTraps = {}
end
local function saveSpeedTraps()
local ok, err = pcall(function()
SaveResourceFile(resourceName, dataFile, json.encode(SpeedTraps, { indent = false }), -1)
end)
if not ok then
print("[wasabi_police] Error saving speedtraps:", err or "unknown")
end
end
-- -------------------------
-- Utility framework-agnostic helpers
-- -------------------------
local function isPlayerPolice(serverId)
-- returns (isPolice, jobName, jobGrade)
serverId = tonumber(serverId)
if not serverId then return false end
if QBCore then
local Player = QBCore.Functions.GetPlayer(serverId)
if Player and Player.PlayerData and Player.PlayerData.job then
local job = Player.PlayerData.job.name
local grade = Player.PlayerData.job.grade and Player.PlayerData.job.grade.level or Player.PlayerData.job.grade
if type(Config.policeJobs) == 'table' then
for _, j in ipairs(Config.policeJobs) do
if job == j or job == ('off' .. j) then return true, job, grade end
end
end
return false, job, grade
end
elseif ESX then
local xPlayer = ESX.GetPlayerFromId(serverId)
if xPlayer and xPlayer.job then
local job = xPlayer.job.name
local grade = xPlayer.job.grade
if type(Config.policeJobs) == 'table' then
for _, j in ipairs(Config.policeJobs) do
if job == j or job == ('off' .. j) then return true, job, grade end
end
end
return false, job, grade
end
else
-- fallback: if Config.policeJobs contains a string equal to "admin" or "dev", you can adapt
return false
end
return false
end
local function updateCopCount()
local players = GetPlayers()
local count = 0
for _, sid in ipairs(players) do
local isCop = select(1, isPlayerPolice(tonumber(sid)))
if isCop then count = count + 1 end
end
OnlinePoliceCount = count
TriggerClientEvent('police:SetCopCount', -1, OnlinePoliceCount)
end
local function syncOfficerGPS()
-- Send full OfficerGPS list to all police clients (so they can create blips)
TriggerClientEvent('wasabi_police:syncOfficerGPS', -1, OfficerGPS)
end
-- -------------------------
-- Callbacks (QBCore) & Generic server-side handlers
-- -------------------------
if QBCore then
-- getPlayerData: client passes list { {id = serverId}, ... } -> server returns { {id = id, name = name}, ...}
QBCore.Functions.CreateCallback('wasabi_police:getPlayerData', function(source, cb, playerList)
local out = {}
for _, v in ipairs(playerList or {}) do
local id = tonumber(v.id)
if id then
local p = QBCore.Functions.GetPlayer(id)
if p then
local name = (p.PlayerData and (p.PlayerData.charinfo and (p.PlayerData.charinfo.firstname .. ' ' .. p.PlayerData.charinfo.lastname)))
or p.PlayerData and (p.PlayerData.firstname and (p.PlayerData.firstname .. ' ' .. p.PlayerData.lastname))
or GetPlayerName(id)
out[#out + 1] = { id = id, name = name }
else
out[#out + 1] = { id = id, name = GetPlayerName(id) or ("Player " .. id) }
end
end
end
cb(out)
end)
QBCore.Functions.CreateCallback('wasabi_police:itemCheck', function(source, cb, item)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then cb(0); return end
local invItem = nil
if Player.Functions and Player.Functions.GetItemByName then
invItem = Player.Functions.GetItemByName(item)
if invItem then cb(invItem.amount or invItem.count or 0); return end
end
-- fallback attempts
if Player.PlayerData and Player.PlayerData.items then
for _, it in ipairs(Player.PlayerData.items) do
if it.name == item then cb(it.amount or it.count or 0); return end
end
end
cb(0)
end)
QBCore.Functions.CreateCallback('wasabi_police:isCuffed', function(source, cb, targetId)
targetId = tonumber(targetId)
cb(targetId and CuffedPlayers[targetId] ~= nil or false)
end)
QBCore.Functions.CreateCallback('wasabi_police:addSpeedTrap', function(source, cb, objectCoords, objHeading, configIndex, input)
local isCop = select(1, isPlayerPolice(source))
if not isCop then cb(false); return end
-- validate input
if not input or not input[1] or not tonumber(input[2]) then cb(false); return end
local id = (#SpeedTraps) + 1
local trap = {
id = id,
coords = objectCoords,
heading = objHeading,
option = configIndex,
name = input[1],
speed = tonumber(input[2]),
creator = source,
createdAt = os.time()
}
SpeedTraps[#SpeedTraps + 1] = trap
saveSpeedTraps()
-- notify all clients to add it
TriggerClientEvent('wasabi_police:addNewSpeedTrap', -1, trap)
cb(true)
end)
QBCore.Functions.CreateCallback('wasabi_police:canPurchase', function(source, cb, data)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then cb(false); return end
-- Example logic: check cash/bank
local price = tonumber(data.price) or tonumber(data.price) or 0
-- If you want purchase to come from society account, implement here (society account check)
if price <= 0 then cb(true); return end
-- Try bank first
local bank = (Player.PlayerData and Player.PlayerData.money and Player.PlayerData.money.bank) or 0
if bank >= price then
-- remove money
Player.Functions.RemoveMoney('bank', price, 'police-armoury-purchase')
cb(true); return
end
-- fallback: cash
local cash = (Player.PlayerData and Player.PlayerData.money and Player.PlayerData.money.cash) or 0
if cash >= price then
Player.Functions.RemoveMoney('cash', price, 'police-armoury-purchase')
cb(true); return
end
cb(false)
end)
end
-- Generic server-side RPC fallback (some frameworks use custom awaitServerCallback bridges)
RegisterNetEvent('wasabi_police:rpc_getPlayerData', function(playerList)
local src = source
local out = {}
for _, v in ipairs(playerList or {}) do
local id = tonumber(v.id)
if id then
local name = GetPlayerName(id) or ('Player ' .. id)
out[#out + 1] = { id = id, name = name }
end
end
TriggerClientEvent('wasabi_police:rpc_getPlayerData:reply', src, out)
end)
-- -------------------------
-- Server events (called from client)
-- -------------------------
RegisterNetEvent('wasabi_police:updateCopCount')
AddEventHandler('wasabi_police:updateCopCount', function()
updateCopCount()
end)
RegisterNetEvent('wasabi_police:addOfficerToGPS')
AddEventHandler('wasabi_police:addOfficerToGPS', function()
local src = source
local isCop = select(1, isPlayerPolice(src))
if not isCop then return end
OfficerGPS[src] = true
syncOfficerGPS()
end)
-- A player can call to stop sharing GPS
RegisterNetEvent('wasabi_police:removeOfficerFromGPS')
AddEventHandler('wasabi_police:removeOfficerFromGPS', function()
local src = source
OfficerGPS[src] = nil
syncOfficerGPS()
end)
RegisterNetEvent('wasabi_police:getPoliceOnline')
AddEventHandler('wasabi_police:getPoliceOnline', function()
local src = source
TriggerClientEvent('police:SetCopCount', src, OnlinePoliceCount)
end)
RegisterNetEvent('wasabi_police:removeSpeedTrap')
AddEventHandler('wasabi_police:removeSpeedTrap', function(id)
local src = source
local isCop = select(1, isPlayerPolice(src))
if not isCop then return end
id = tonumber(id)
if not id then return end
for i, v in ipairs(SpeedTraps) do
if v.id == id then
table.remove(SpeedTraps, i)
saveSpeedTraps()
TriggerClientEvent('wasabi_police:removeSpeedTrap', -1, id)
return
end
end
end)
RegisterNetEvent('wasabi_police:renameSpeedTrap')
AddEventHandler('wasabi_police:renameSpeedTrap', function(id, name)
local src = source
local isCop = select(1, isPlayerPolice(src))
if not isCop then return end
id = tonumber(id)
if not id then return end
for i, v in ipairs(SpeedTraps) do
if v.id == id then
v.name = tostring(name)
saveSpeedTraps()
TriggerClientEvent('wasabi_police:updateSpeedTrapName', -1, id, v.name)
return
end
end
end)
RegisterNetEvent('wasabi_police:setGSR')
AddEventHandler('wasabi_police:setGSR', function(state)
local src = source
state = state and true or false
if state then
GSRPlayers[src] = { positive = true, ts = os.time() }
else
GSRPlayers[src] = nil
end
-- notify cops optionally
TriggerClientEvent('wasabi_police:gsrUpdate', -1, { id = src, positive = state })
end)
RegisterNetEvent('wasabi_police:lockpickHandcuffs')
AddEventHandler('wasabi_police:lockpickHandcuffs', function(targetServerId)
local src = source
local isCop = select(1, isPlayerPolice(src))
-- lockpicking could be done by anyone if allowed; adapt to config
if not targetServerId then return end
targetServerId = tonumber(targetServerId)
-- If target is cuffed, uncuff them
if CuffedPlayers[targetServerId] then
CuffedPlayers[targetServerId] = nil
-- notify target to uncuff themselves
TriggerClientEvent('wasabi_police:uncuff', targetServerId)
-- play animation on the lockpicker (client side should handle 'uncuffAnim' if needed)
TriggerClientEvent('wasabi_police:uncuffAnim', src, targetServerId)
end
end)
RegisterNetEvent('wasabi_police:breakLockpick')
AddEventHandler('wasabi_police:breakLockpick', function()
local src = source
-- remove one lockpick from source (QBCore / ESX examples)
if QBCore then
local Player = QBCore.Functions.GetPlayer(src)
if Player and Player.Functions then
Player.Functions.RemoveItem('lockpick', 1)
TriggerClientEvent('wasabi_bridge:notify', src, "Lockpick cassé", "Votre casse-outil s'est brisé.", 'error')
end
elseif ESX then
local xPlayer = ESX.GetPlayerFromId(src)
if xPlayer then
xPlayer.removeInventoryItem('lockpick', 1)
TriggerClientEvent('wasabi_bridge:notify', src, "Lockpick cassé", "Votre casse-outil s'est brisé.", 'error')
end
end
end)
-- put someone in vehicle: officer calls server with target id => server asks target client to put themselves in vehicle
RegisterNetEvent('wasabi_police:inVehiclePlayer')
AddEventHandler('wasabi_police:inVehiclePlayer', function(targetServerId)
local src = source
local isCop = select(1, isPlayerPolice(src))
if not isCop then return end
targetServerId = tonumber(targetServerId)
if not targetServerId then return end
TriggerClientEvent('wasabi_police

utInVehicle', targetServerId)
end)
-- take someone out of vehicle
RegisterNetEvent('wasabi_police

utVehiclePlayer')
AddEventHandler('wasabi_police

utVehiclePlayer', function(targetServerId)
local src = source
local isCop = select(1, isPlayerPolice(src))
if not isCop then return end
targetServerId = tonumber(targetServerId)
if not targetServerId then return end
-- client expects policeID param
TriggerClientEvent('wasabi_police:takeFromVehicle', targetServerId, src)
end)
-- seizure of cash (simplified): remove cash from target and give to officer or society
RegisterNetEvent('wasabi_police:seizeCash')
AddEventHandler('wasabi_police:seizeCash', function(targetServerId)
local src = source
local isCop = select(1, isPlayerPolice(src))
if not isCop then return end
targetServerId = tonumber(targetServerId)
if not targetServerId then return end
-- Example: transfer all cash from target to officer (or society). Adapt to your economy.
if QBCore then
local target = QBCore.Functions.GetPlayer(targetServerId)
local officer = QBCore.Functions.GetPlayer(src)
if target and officer then
local cash = (target.PlayerData.money.cash or 0)
if cash and cash > 0 then
target.Functions.RemoveMoney('cash', cash, 'seized-by-police')
officer.Functions.AddMoney('cash', cash, 'seized-from-player')
TriggerClientEvent('wasabi_bridge:notify', src, "Argent saisi", ("Vous avez saisi $%s"):format(cash), 'success')
TriggerClientEvent('wasabi_bridge:notify', targetServerId, "Argent saisi", "La police vous a saisi de l'argent.", 'error')
else
TriggerClientEvent('wasabi_bridge:notify', src, "Rien à saisir", "Le joueur n'a pas d'argent liquide.", 'error')
end
end
elseif ESX then
local tx = ESX.GetPlayerFromId(targetServerId)
local ox = ESX.GetPlayerFromId(src)
if tx and ox then
local cash = tx.getMoney()
if cash and cash > 0 then
tx.removeMoney(cash)
ox.addMoney(cash)
TriggerClientEvent('wasabi_bridge:notify', src, "Argent saisi", ("Vous avez saisi $%s"):format(cash), 'success')
TriggerClientEvent('wasabi_bridge:notify', targetServerId, "Argent saisi", "La police vous a saisi de l'argent.", 'error')
else
TriggerClientEvent('wasabi_bridge:notify', src, "Rien à saisir", "Le joueur n'a pas d'argent liquide.", 'error')
end
end
else
TriggerClientEvent('wasabi_bridge:notify', src, "Seize unavailable", "No economy integration present.", 'error')
end
end)
-- Escort / release
RegisterNetEvent('wasabi_police:releasePlayer')
AddEventHandler('wasabi_police:releasePlayer', function(targetServerId)
local src = source
local isCop = select(1, isPlayerPolice(src))
-- It may be called by the escorting officer or client on death; allow if officer (or optional: allow any)
if not isCop then return end
targetServerId = tonumber(targetServerId)
if not targetServerId then return end
-- Tell the escorted target to free themselves
TriggerClientEvent('wasabi_police:releasePlayerFromEscort', targetServerId, src)
-- Tell the officer to stop escorting (client will verify target id)
TriggerClientEvent('wasabi_police:stopEscorting', src, targetServerId)
end)
RegisterNetEvent('wasabi_police:escortPlayerStop')
AddEventHandler('wasabi_police:escortPlayerStop', function(targetServerId, forced)
local src = source
local isCop = select(1, isPlayerPolice(src))
if not isCop then return end
targetServerId = tonumber(targetServerId)
if not targetServerId then return end
TriggerClientEvent('wasabi_police:releasePlayerFromEscort', targetServerId, src)
TriggerClientEvent('wasabi_police:stopEscorting', src, targetServerId)
end)
-- toggle duty (server side)
RegisterNetEvent('wasabi_police:svToggleDuty')
AddEventHandler('wasabi_police:svToggleDuty', function(jobName, grade)
local src = source
if not jobName then return end
-- server stores on-duty map and triggers cop-count update
if OnDutyPlayers[src] then
-- remove
OnDutyPlayers[src] = nil
OfficerGPS[src] = nil
else
OnDutyPlayers[src] = { job = jobName, grade = grade }
-- optionally auto add to GPS if config
if Config.GPSBlips and Config.GPSBlips.enabled and not Config.GPSBlips.item then
OfficerGPS[src] = true
end
end
updateCopCount()
syncOfficerGPS()
end)
-- Weapon license management (basic)
RegisterNetEvent('wasabi_police:weaponLicense')
AddEventHandler('wasabi_police:weaponLicense', function(targetId)
local src = source
local isCop, jobName, grade = select(1, isPlayerPolice(src))
if not isCop then return end
-- permission check: grade min
local minGrade = (Config.GrantWeaponLicenses and Config.GrantWeaponLicenses.minGrade) or 0
if tonumber(grade) < tonumber(minGrade) then
TriggerClientEvent('wasabi_bridge:notify', src, "Grade trop bas", "Vous n'avez pas la permission.", 'error')
return
end
targetId = tonumber(targetId)
if not targetId then return end
-- Store license server-side (for demo). Integrate DB here as needed.
WeaponLicenses[targetId] = WeaponLicenses[targetId] or {}
WeaponLicenses[targetId].weapon = true
-- optionally persist to file or database
TriggerClientEvent('wasabi_bridge:notify', src, "Licence accordée", ("Vous avez accordé une licence à %s"):format(targetId), 'success')
TriggerClientEvent('wasabi_bridge:notify', targetId, "Licence reçue", "Vous avez reçu une licence d'arme.", 'success')
end)
RegisterNetEvent('wasabi_police:revokeWeaponLicense')
AddEventHandler('wasabi_police:revokeWeaponLicense', function(targetId)
local src = source
local isCop = select(1, isPlayerPolice(src))
if not isCop then return end
targetId = tonumber(targetId)
if not targetId then return end
if WeaponLicenses[targetId] then WeaponLicenses[targetId] = nil end
TriggerClientEvent('wasabi_bridge:notify', src, "Licence retirée", ("Licence retirée pour %s"):format(targetId), 'success')
TriggerClientEvent('wasabi_bridge:notify', targetId, "Licence retirée", "Votre licence d'arme a été retirée.", 'error')
end)
-- When a client asks for initial SpeedTraps, send them
RegisterNetEvent('wasabi_police:requestInitSpeedTraps')
AddEventHandler('wasabi_police:requestInitSpeedTraps', function()
local src = source
TriggerClientEvent('wasabi_police:initSpeedTraps', src, SpeedTraps)
end)
-- -------------------------
-- Resource / player lifecycle
-- -------------------------
AddEventHandler('playerDropped', function(reason)
local src = source
-- cleanup gps/duty/cuffed
OfficerGPS[src] = nil
OnDutyPlayers[src] = nil
CuffedPlayers[src] = nil
GSRPlayers[src] = nil
updateCopCount()
syncOfficerGPS()
end)
AddEventHandler('onResourceStart', function(resName)
if resName ~= resourceName then return end
loadSpeedTraps()
-- broadcast initial list to connected players
Citizen.SetTimeout(1500, function()
local players = GetPlayers()
for _, p in ipairs(players) do
TriggerClientEvent('wasabi_police:initSpeedTraps', p, SpeedTraps)
TriggerClientEvent('police:SetCopCount', p, OnlinePoliceCount)
end
end)
end)
-- On server start load speedtraps immediately
loadSpeedTraps()
Citizen.CreateThread(function() updateCopCount() end)
-- -------------------------
-- Convenience exports (optional)
-- -------------------------
exports('GetSpeedTraps', function()
return SpeedTraps
end)
exports('IsPlayerCuffed', function(serverId)
return CuffedPlayers[tonumber(serverId)] ~= nil
end)
-- -------------------------
-- Notes for integrators:
-- - adapt economy interactions (seizeCash, canPurchase) to your server's rules (society account, evidence stashes, etc.)
-- - persist WeaponLicenses if you need permanence (database)
-- - you can extend OfficerGPS to store player's coords and broadcast smaller packets
-- -------------------------
print(("[wasabi_police] Server ready (%s). QBCore=%s ESX=%s"):format(resourceName, tostring(QBCore ~= nil), tostring(ESX ~= nil)))