Files
ND_Core/server/vehicle.lua
Andyyy7666 b837771300 refactor(vehicles): move garages to separate resource keep api
- Moved traffic locking to client
- removed garages (moved to ND_Garages resource)
- optimized and improved the api
- fixed smaller bugs
2026-03-16 08:07:43 +01:00

825 lines
26 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
local ox_inventory
local inventoryStarted = false
local spawnedPlayerVehicles = {}
local vehicleTypes = json.decode(GetResourceKvpString("ND_Core:vehTypes") or "[]")
NDCore.isResourceStarted("ox_inventory", function(started)
inventoryStarted = started
if not started then return end
ox_inventory = exports.ox_inventory
end)
local function getVehicleType(coords, model)
if vehicleTypes[model] then
return vehicleTypes[model]
end
local tempVehicle = CreateVehicle(model, coords.x, coords.y, coords.z-5.0, coords.w, true, false)
local time = os.time()
while not DoesEntityExist(tempVehicle) and os.time()-time < 5 do Wait(0) end
if not DoesEntityExist(tempVehicle) then return end
local vehType = GetVehicleType(tempVehicle)
DeleteEntity(tempVehicle)
vehicleTypes[model] = vehType
SetResourceKvp("ND_Core:vehTypes", json.encode(vehicleTypes))
return vehType
end
local function generatePlate()
local plate = {}
local i = 1
local p = 1
-- ensure pattern exists and pad to 8 chars
local pattern = Config.platePattern
if #pattern < 8 then
pattern = pattern .. string.rep(" ", 8 - #pattern)
end
while i <= 8 do
local char = pattern:sub(p, p)
if char == "^" then
-- literal next character
p = p + 1
plate[i] = pattern:sub(p, p)
elseif char == "1" then
plate[i] = tostring(math.random(0, 9))
elseif char == "A" then
plate[i] = string.char(math.random(65, 90)) -- AZ
elseif char == "." then
if math.random(0, 1) == 0 then
plate[i] = tostring(math.random(0, 9))
else
plate[i] = string.char(math.random(65, 90))
end
else
plate[i] = char
end
i = i + 1
p = p + 1
end
return table.concat(plate)
end
local function generateVehiclePlate(newPlate)
local plate = newPlate or generatePlate()
while MySQL.scalar.await("SELECT 1 FROM nd_vehicles WHERE plate = ?", {plate}) do
plate = generatePlate()
end
return plate
end
local function generateTemporaryVehicleId()
return ("temp_%s%d"):format(string.char(math.random(65, 90)), math.random(1, 999999))
end
local function getVehicleDatabaseInfo(vehicle)
if not vehicle then return end
local stored = vehicle.stored == 1
local impounded = vehicle.impounded == 1
local properties = json.decode(vehicle.properties) or {}
properties.plate = vehicle.plate
return {
id = vehicle.id,
owner = vehicle.owner,
plate = vehicle.plate,
properties = properties,
stored = stored,
impounded = impounded,
stolen = vehicle.stolen == 1,
available = not impounded and stored,
metadata = json.decode(vehicle.metadata) or {}
}
end
function NDCore.generateVehiclePlate()
return generatePlate()
end
function NDCore.generateUniqueVehiclePlate()
return generateVehiclePlate()
end
--- get vehicle information queried from the database by the vehicle id
---@param vehicleId string | numb
---@return table | nil
function NDCore.getVehicleById(vehicleId)
local result = MySQL.query.await("SELECT * FROM nd_vehicles WHERE id = ?", {vehicleId})
return getVehicleDatabaseInfo(result?[1])
end
--- get vehicle information and functions
---@param entity number
---@return table
function NDCore.getVehicle(entity)
if not DoesEntityExist(entity) then return end
local state = Entity(entity).state
local self = {
entity = entity,
id = state.id,
owner = state.owner,
keys = state.keys,
properties = state.props,
locked = state.locked,
hotwired = state.hotwired,
metadata = state.metadata or {},
netId = NetworkGetNetworkIdFromEntity(entity)
}
-- save properties to db
function self.saveProperties(props, propsToSave)
local properties = {}
local vehProps = props or lib.callback.await("ND_Vehicles:getProps", NetworkGetEntityOwner(entity), self.netId)
if propsToSave then
properties = self.properties
if vehProps then
for prop, value in pairs(vehProps) do
if lib.table.contains(propsToSave, prop) then
properties[prop] = value
end
end
end
else
properties = vehProps
end
if self.properties?.callsign then
properties.callsign = true
end
self.properties = properties
state.props = properties
MySQL.query("UPDATE nd_vehicles SET properties = ? WHERE id = ?", {json.encode(properties), self.id})
end
--- delete the vehicle
function self.delete(saveProperties, propsToSave)
if not DoesEntityExist(entity) then return end
if saveProperties and self.id and self.owner then
self.saveProperties(false, propsToSave)
end
if DoesEntityExist(entity) then
DeleteEntity(entity)
end
return true
end
-- remvoe vehicle from db.
function self.remove(keepEntity)
if not keepEntity and DoesEntityExist(entity) then
DeleteEntity(entity)
end
MySQL.query("DELETE FROM nd_vehicles WHERE id = ?", {self.id})
end
--- set vehicle properties
---@param props table
function self.setProperties(props)
if not DoesEntityExist(entity) then return end
local properties = type(props) == "string" and json.decode(props) or props
local state = Entity(entity).state
if self.properties?.callsign then
properties.callsign = true
end
state.allPropsApplied = false
state.props = properties
self.properties = properties
if not self.id or not self.owner then return end
self.saveProperties(properties)
end
--- set vehicle locked/unlocked
---@param status boolean
function self.setLocked(status)
if not DoesEntityExist(entity) then return end
local state = Entity(entity).state
state.locked = status
self.locked = status
end
--- update the vehicle plate
---@param plate string
---@return boolean
function self.setPlate(plate)
if not DoesEntityExist(entity) or MySQL.scalar.await("SELECT 1 FROM nd_vehicles WHERE plate = ?", {plate}) then return end
if self.properties then
self.properties.plate = plate
local state = Entity(entity).state
state.allPropsApplied = false
state.props = self.properties
end
if not self.id or not self.owner then return end
MySQL.query("UPDATE nd_vehicles SET plate = ? WHERE id = ?", {plate, self.id})
return true
end
--- set the vehicle availability status
---@param statusType string
---@param status boolean
function self.setStatus(statusType, status, keepEntity)
if not lib.table.contains({"stored", "impounded", "stolen"}, statusType) then return end
if statusType ~= "stolen" and not keepEntity then self.delete(true) end
if not self.id or not self.owner then return end
local query = ("UPDATE nd_vehicles SET %s = ? WHERE id = ?"):format(statusType)
MySQL.query(query, {status and 1 or 0, self.id})
return true
end
function self.setMetadata(key, value)
self.metadata[key] = value
if DoesEntityExist(entity) then
local state = Entity(self.entity).state
if not state.metadata then
state.metadata = self.metadata
else
state.metadata[key] = value
end
end
if not self.id or not self.owner then
lib.print.debug(('Skipped storing metadata for vehicle %s as it does not have an id/owner associated with it.'):format(self.entity))
return
end
MySQL.query("UPDATE nd_vehicles SET metadata = ? WHERE id = ?", {json.encode(self.metadata), self.id})
end
if self.id and self.owner then
spawnedPlayerVehicles[self.id] = self
end
return self
end
--- get a characters owned vehicles
---@param characterId number
---@return table
function NDCore.getVehicles(characterId)
local result = MySQL.query.await("SELECT * FROM nd_vehicles WHERE owner = ?", {characterId})
if not result then return {} end
local vehicles = {}
for _, vehicle in pairs(result) do
local info = getVehicleDatabaseInfo(vehicle)
if info then
vehicles[#vehicles+1] = info
end
end
return vehicles
end
--- give a player keys/access to a vehicle
---@param source number
---@param vehicle number
---@param access boolean
---@param info table
function NDCore.giveVehicleAccess(source, vehicle, access, info)
if not vehicle or not DoesEntityExist(vehicle) then return end
local state = Entity(vehicle).state
local netId = info?.netId or NetworkGetNetworkIdFromEntity(vehicle)
local vehicleId = info?.vehicleId or state.id or generateTemporaryVehicleId()
if not state.id then
state.id = vehicleId
end
local player = NDCore.getPlayer(source)
if player then
local keys = state.keys or {}
keys[player.id] = access
state.keys = keys
end
if not inventoryStarted or not Config.useInventoryForKeys then return end
local hasKey = ox_inventory:GetSlotIdWithItem(source, "keys", {
vehId = vehicleId
})
if access and not hasKey then
local plate = info?.plate or GetVehicleNumberPlateText(vehicle)
local model = info?.model or GetEntityModel(vehicle)
local modelName = info?.modelName or model and lib.callback.await("ND_Vehicles:getVehicleModelMakeLabel", source, model) or ""
ox_inventory:AddItem(source, "keys", 1, {
vehOwner = owner or state.owner,
vehId = vehicleId,
vehPlate = plate,
vehModel = modelName,
keyEnabled = true,
vehNetId = netId
})
elseif not access and hasKey then
ox_inventory:RemoveItem(source, "keys", 1, nil, hasKey)
end
end
--- spawn a vehicle, set info & give keys.
---@param info table
---@return table
function NDCore.createVehicle(info)
local owner = info.owner
local vehicleId = info.vehicleId or generateTemporaryVehicleId()
local properties = info.properties or {}
local coords = info.coords
local spawnCoords = coords
local coordType = type(coords)
if coordType == "vector3" or coordType == "table" then
spawnCoords = vector4(coords.x, coords.y, coords.z, coords.w or coords.h or info.heading or 0.0)
end
if not spawnCoords then
return Citizen.Trace("NDCore.createVehicle", "spawnCoords not found")
end
local model = info.model or properties.model
local vehType = getVehicleType(spawnCoords, model)
if not vehType then
return Citizen.Trace("NDCore.createVehicle", "vehType not found")
end
local veh = CreateVehicleServerSetter(model, vehType, spawnCoords.x, spawnCoords.y, spawnCoords.z, spawnCoords.w)
local time = os.time()
while not DoesEntityExist(veh) and os.time()-time < 5 do Wait(5) end
if not veh or not DoesEntityExist(veh) then
return Citizen.Trace("NDCore.createVehicle", "vehicle entity doesn't exist")
end
local netId = NetworkGetNetworkIdFromEntity(veh)
local state = Entity(veh).state
state.locked = true
local keys = info.keys or {}
if not properties.plate or info.plate then
properties.plate = info.plate or generateVehiclePlate()
end
if owner then
keys[owner] = true
state.owner = owner
end
if info.blipGroups then
state.blipGroups = info.blipGroups
end
state.keys = keys
state.id = vehicleId
if not info.ignoreProps then
state.props = properties
end
local vehicleName
if inventoryStarted and Config.useInventoryForKeys then
for charId, _ in pairs(keys) do
local playerSource = charId == owner and info.source
if not playerSource then
local player = NDCore.getPlayers("id", charId, true)[1]
playerSource = player and player.source
end
if playerSource then
if not vehicleName then
vehicleName = lib.callback.await("ND_Vehicles:getVehicleModelMakeLabel", playerSource, model) or ""
end
NDCore.giveVehicleAccess(playerSource, veh, true, {
vehicleId = vehicleId,
netId = netId,
plate = info.plate or properties?.plate,
model = model,
vehicleName = vehicleName,
owner = owner
})
end
end
end
for i=1, 3 do
if DoesEntityExist(veh) then
SetVehicleNumberPlateText(veh, properties.plate)
end
Wait(500)
end
return NDCore.getVehicle(veh)
end
--- transfer ownership of a vehicle between players
---@param vehicleId number|string
---@param fromSource number
---@param toSource number
---@return boolean
function NDCore.transferVehicleOwnership(vehicleId, fromSource, toSource)
local playerFrom = NDCore.getPlayer(fromSource)
local playerTo = NDCore.getPlayer(toSource)
if not playerFrom or not playerTo then return end
local vehicle = NDCore.getVehicleById(vehicleId)
if not vehicle or vehicle.owner ~= playerFrom.id then return end
MySQL.query.await("UPDATE nd_vehicles SET owner = ? WHERE id = ?", {playerTo.id, vehicleId})
local vehicleInfo = spawnedPlayerVehicles[vehicleId]
if vehicleInfo then
local veh, netId, model = vehicleInfo.entity, vehicleInfo.netId, GetEntityModel(veh)
playerFrom.triggerEvent("ND_Vehicles:blip", netId, false)
playerTo.triggerEvent("ND_Vehicles:blip", netId, true)
NDCore.giveVehicleAccess(fromSource, veh, false, {
vehicleId = vehicleId,
netId = netId,
model = model,
owner = playerFrom.id
})
local state = Entity(veh).state
state.owner = playerTo.id
NDCore.giveVehicleAccess(toSource, veh, true, {
vehicleId = vehicleId,
netId = netId,
model = model,
owner = playerTo.id
})
end
playerFrom.notify({
title = locale("vehicle_ownership_transfered"),
description = locale("vehicle_ownership_transfered2", vehicle.plate),
position = "bottom-right",
type = "success"
})
playerTo.notify({
title = locale("vehicle_ownership_received"),
description = locale("vehicle_ownership_received2", vehicle.plate),
position = "bottom-right"
})
return true
end
--- set vehicle as owned by player
---@param playerId number
---@param properties table
---@param stored boolean
---@return vehicleId number
function NDCore.setVehicleOwned(playerId, properties, stored, setPlate)
local plate = setPlate or generateVehiclePlate()
properties.plate = plate
return MySQL.insert.await("INSERT INTO nd_vehicles (owner, plate, properties, stored) VALUES (?, ?, ?, ?)", {playerId, plate, json.encode(properties), stored and 1 or 0})
end
--- give a player access to another players vehicle as if they are next to eachother and hand the keys.
---@param source number
---@param target number
---@param vehicle number
---@return boolean
function NDCore.shareVehicleKeys(source, target, vehicle)
if not vehicle or not DoesEntityExist(vehicle) then return end
local player = NDCore.getPlayer(source)
local targetPlayer = NDCore.getPlayer(target)
local state = Entity(vehicle).state
if not targetPlayer or not player or player.id ~= state.owner then return end
NDCore.giveVehicleAccess(target, vehicle, true)
local plate = GetVehicleNumberPlateText(vehicle)
player.notify({
title = locale("keys_shared"),
description = locale("keys_shared2", plate),
position = "bottom-right",
type = "success"
})
targetPlayer.notify({
title = locale("keys_received"),
description = locale("keys_received2", plate),
position = "bottom-right"
})
return true
end
--- spawned a vehicle that's owned by the player and checks for availability
---@param source number
---@param vehicleId number
---@param coords vector4
---@return table
function NDCore.spawnOwnedVehicle(source, vehicleId, coords, heading, _vehicle)
local player = NDCore.getPlayer(source)
if not player then return end
local vehicle = _vehicle or NDCore.getVehicleById(vehicleId)
if not vehicle or vehicle.owner ~= player.id then return end
if not vehicle.available and not vehicle.impounded then return end
MySQL.query.await("UPDATE nd_vehicles SET stored = ? WHERE id = ?", {0, vehicleId})
local properties = vehicle.properties
if properties.callsign then
local callsign = player.getMetadata("callsign")
if not callsign or callsign == "" then goto skip end
callsign = tostring(callsign)
if not callsign or callsign:len() ~= 3 then goto skip end
properties.modFender = tonumber(callsign:sub(1, 1))
properties.modRightFender = tonumber(callsign:sub(2, 2))
properties.modRoof = tonumber(callsign:sub(3, 3))
end
::skip::
return NDCore.createVehicle({
owner = player.id,
model = properties.model,
coords = vec4(coords.x, coords.y, coords.z, coords.w or coords.heading or heading),
properties = properties,
vehicleId = vehicleId,
source = source
})
end
local function toggleVehicleLock(source, entity, nearby, metadata)
local player = NDCore.getPlayer(source)
if metadata and not metadata.keyEnabled then
return player.notify({
title = locale("no_signal"),
description = locale("veh_key_disabled"),
type = "error",
position = "bottom-right",
duration = 3000
})
elseif not nearby then
return player.notify({
title = locale("no_signal"),
description = locale("veh_far_away"),
type = "error",
position = "bottom-right",
duration = 3000
})
end
local vehicle = NDCore.getVehicle(entity)
local locked = not vehicle.locked
vehicle.setLocked(locked)
player.triggerEvent("ND_Vehicles:keyFob", vehicle.netId)
if locked then
return player.notify({
title = locale("veh_locked"),
description = locale("veh_locked2"),
type = "success",
position = "bottom-right",
duration = 3000
})
end
player.notify({
title = locale("veh_unlocked"),
description = locale("veh_unlocked2"),
type = "inform",
position = "bottom-right",
duration = 3000
})
end
local function getNearbyVehicles(coords, range)
local nearby = {}
local vehicles = GetAllVehicles()
for i=1, #vehicles do
local veh = vehicles[i]
local vehCoords = GetEntityCoords(veh)
if #(coords-vehCoords) < range then
nearby[#nearby+1] = veh
end
end
return nearby
end
local function lockNearestVehicle(source, vehId, metadata)
local ped = GetPlayerPed(source)
local pedCoords = GetEntityCoords(ped)
local veh = spawnedPlayerVehicles[vehId]?.entity
if veh and DoesEntityExist(veh) then
local vehCoords = GetEntityCoords(veh)
if not pedCoords or not vehCoords then return end
return toggleVehicleLock(source, veh, #(pedCoords-vehCoords) < 25.0, metadata)
end
local vehicles = getNearbyVehicles(pedCoords, 25.0)
for i=1, #vehicles do
local veh = vehicles[i]
local state = Entity(veh).state
if state and state.id == vehId then
return toggleVehicleLock(source, veh, true, metadata)
end
end
end
local function hasBlipGroup(player, groups)
groups = groups or {}
for i=1, #groups do
local group = groups[i]
if player.groups[group] then
return true
end
end
end
--- inventory keys using item.
exports("keys", function(event, item, inventory, slot, data)
if event ~= "usingItem" or not Config.useInventoryForKeys or not inventoryStarted then return end
local metadata
for i=1, #inventory.items do
local item = inventory.items[i]
if item and item.slot == slot then
metadata = item.metadata
break
end
end
if not metadata then return false end
lockNearestVehicle(inventory.id, metadata.vehId, metadata)
return false
end)
-- if using inventory and inventory keys players can spawn keys when in their vehicle with this command.
RegisterCommand("getkeys", function(source, args, rawCommand)
if not Config.useInventoryForKeys or not inventoryStarted then return end
local veh = GetVehiclePedIsIn(GetPlayerPed(source))
if not veh or veh == 0 then return end
local player = NDCore.getPlayer(source)
local state = Entity(veh).state
local owner = state.owner
local blipGroups = state.blipGroups
if not state.id or (not owner or owner ~= player.id) and (not hasBlipGroup(player, blipGroups)) then return end
local props = state.props
ox_inventory:AddItem(source, "keys", 1, {
vehOwner = owner,
vehId = state.id,
vehPlate = props.plate,
vehModel = lib.callback.await("ND_Vehicles:getVehicleModelMakeLabel", source, props.model),
keyEnabled = true,
vehNetId = NetworkGetNetworkIdFromEntity(veh)
})
end, false)
-- key sharing if not using inventory or inventory keys.
RegisterCommand("givekeys", function(source, args, rawCommand)
if Config.useInventoryForKeys and inventoryStarted then return end
local src = source
if not args[1] then return end
local target = tonumber(args[1])
if not GetPlayerPing(target) then return end
local veh = GetVehiclePedIsIn(GetPlayerPed(src))
if veh == 0 then
veh = GetVehiclePedIsIn(GetPlayerPed(src), true)
if veh == 0 then return end
end
NDCore.shareVehicleKeys(src, target, veh)
end, false)
-- lock/unlock vehicles if they're within range.
RegisterNetEvent("ND_Vehicles:toggleVehicleLock", function(netId)
if Config.useInventoryForKeys and inventoryStarted then return end
local src = source
local veh = NetworkGetEntityFromNetworkId(netId)
if not veh or not DoesEntityExist(veh) then return end
local ped = GetPlayerPed(src)
local pedCoords = GetEntityCoords(ped)
local vehCoords = GetEntityCoords(veh)
if not pedCoords or not vehCoords then return end
toggleVehicleLock(src, veh, #(pedCoords-vehCoords) < 25.0)
end)
-- locking of npc vehicles, if the players spawns inside a vehicle it won't be locked.
-- AddEventHandler("entityCreated", function(entity)
-- local time = os.time()
-- while not DoesEntityExist(entity) and os.time()-time < 5 do Wait(50) end
-- if not DoesEntityExist(entity) or GetEntityType(entity) ~= 2 then return end
-- local state = Entity(entity).state
-- if state.owner or state.locked ~= nil then return end
-- time = os.time()
-- local driver = GetPedInVehicleSeat(entity, -1)
-- while DoesEntityExist(entity) and driver == 0 and os.time()-time < 2 do
-- driver = GetPedInVehicleSeat(entity, -1)
-- Wait(100)
-- end
-- if not DoesEntityExist(entity) then return end
-- if DoesEntityExist(driver) and IsPedAPlayer(driver) then
-- state.locked = false
-- state.hotwired = true
-- end
-- if math.random(1, 100) <= Config.randomUnlockedVehicleChance then return end
-- state.locked = true
-- end)
-- disables inventory vehicles keys, disabled vehicles keys can no longer be used. Kinda like taking the battery out.
RegisterNetEvent("ND_Vehicles:disableKey", function(slot)
local src = source
local key = ox_inventory:GetSlot(src, slot)
local metadata = key.metadata
if not metadata.keyEnabled then return end
metadata.keyEnabled = false
ox_inventory:SetMetadata(src, slot, key.metadata)
end)
-- sync alarm when vehicle is lockpicked.
RegisterNetEvent("ND_Vehicles:lockpick", function(netId, success)
local src = source
local veh = NetworkGetEntityFromNetworkId(netId)
if not veh or not DoesEntityExist(veh) then return end
local ped = GetPlayerPed(src)
local pedCoords = GetEntityCoords(ped)
local vehCoords = GetEntityCoords(veh)
if #(pedCoords-vehCoords) > 5.0 then return end
local owner = NetworkGetEntityOwner(veh)
TriggerClientEvent("ND_Vehicles:syncAlarm", owner, netId)
if not success then return end
local state = Entity(veh).state
state.locked = false
end)
-- sync alarm if vehicle gets hotwired.
RegisterNetEvent("ND_Vehicles:hotwire", function(netId)
local src = source
local ped = GetPlayerPed(src)
local playerVeh = GetVehiclePedIsIn(ped)
local veh = NetworkGetEntityFromNetworkId(netId)
if not playerVeh or playerVeh == 0 or playerVeh ~= veh then return end
local state = Entity(veh).state
state.hotwired = true
end)
RegisterNetEvent("ND_Vehicles:propsApplied", function(netId)
local veh = NetworkGetEntityFromNetworkId(netId)
if not veh or not DoesEntityExist(veh) then return end
local state = Entity(veh).state
state:set("allPropsApplied", true, true)
end)
lib.callback.register("ND_Vehicles:getOwnedVehicles", function(src)
local player = NDCore.getPlayer(src)
if not player then return end
return NDCore.getVehicles(player.id)
end)
AddEventHandler("ND:characterLoaded", function(player)
local ownedExistingVehicles = {}
local ownedVehicles = NDCore.getVehicles(player.id)
local vehicles = GetAllVehicles()
local vehiclesToImpound = {}
for i=1, #vehicles do
local veh = vehicles[i]
local state = Entity(veh).state
if state.owner == player.id then
ownedExistingVehicles[#ownedExistingVehicles+1] = state.id
end
end
for i=1, #ownedVehicles do
local veh = ownedVehicles[i]
if veh and not veh.stored and not veh.impounded and not lib.table.contains(ownedExistingVehicles, veh.id) then
vehiclesToImpound[#vehiclesToImpound+1] = veh.id
end
end
if #vehiclesToImpound == 0 then return end
local query = ("UPDATE nd_vehicles SET stored = ? WHERE owner = ? AND id IN (%s)"):format(table.concat(vehiclesToImpound, ", "))
MySQL.rawExecute(query, {1, player.id})
end)