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)) -- A–Z 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)